* fix(platform): discover Windows fnm-managed Node in the toolchain bin list
A GUI-launched packaged app on Windows inherits a stripped PATH and reads no
shell rc, so it relies on wellKnownUserToolchainBins to locate user toolchains.
That list only probed POSIX fnm roots (~/.fnm and ~/.local/share/fnm) with
[installation, bin] segments. fnm on Windows keeps Node under
%APPDATA%\fnm\node-versions\<version>\installation with node.exe directly in
`installation` (no POSIX-style bin subdir), so fnm-managed Node — and every
agent CLI that needs it — was silently undetected.
Probe %APPDATA%\fnm\node-versions (FNM_DIR overrides the root) with the
Windows [installation] segment on win32, alongside the existing POSIX roots.
* fix(platform): also probe %LOCALAPPDATA%\fnm and surface npm-global CLIs (#3062)
fnm on Windows can root under %LOCALAPPDATA%\fnm as well as %APPDATA%\fnm.
Probe both (FNM_DIR still overrides both). Because a globally `npm i -g`'d CLI
shim such as codex.cmd lands in the node version's `installation` dir next to
node.exe, surfacing that dir also makes those CLIs discoverable from a GUI
launch — addressing the "Codex CLI not detected from Windows shell" report.
posthog-node defaults disableGeoip:true (it assumes a datacenter deployment
whose ingestion IP is not the user's). Open Design's daemon runs on the user's
own machine, so the request IP is the user's real public IP and GeoIP is
accurate — identical to what posthog-js already sends. Leaving the default on
stripped country from every daemon-emitted event (run_created, run_finished,
*_result, crash/safety events): ~2.2M events over 30d sat at 0% event-level
country while web events were 100% enriched.
Pass disableGeoip:false on the PostHog node constructor and add a regression
assertion that the constructor receives it.
Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
Close stream-json run stdin as soon as cancellation starts so Claude exits promptly instead of continuing to burn tokens until the process watchdog or account limits stop it. Also add regression coverage for cancel and shutdown paths to ensure stdin closes before SIGTERM.
Generated-By: looper 0.9.10 (runner=worker, agent=opencode)
The P0 'manual edit' UI tests intermittently time out at 30s on
`frame.locator(selector).click()` inside selectPreviewElementThroughBridge
(seen dequeuing #4497 and #4501 from the merge queue, on unrelated
landing-only / docs-only changes).
Root cause is a click-vs-resettle race, not a product bug: entering manual-edit
mode re-injects the edit bridge and re-emits its targets a tick later
(`setTimeout(postTargets, 0)` in edit-mode/bridge.ts) while the preview iframe
is still settling, so the first click is occasionally swallowed and then hangs
on Playwright's post-click stability check until the test timeout.
Wrap the click + selection assertion in expect(...).toPass() with a short
per-attempt timeout, so a single dropped click is retried through the settle
window instead of failing the run. Final assertions are unchanged.
Flaky by nature, so no deterministic red spec; verified e2e typecheck and
that the wrapped assertions match the original. CI 'UI P0 (project-runtime)'
exercises the live path.
Cursor Agent's adapter passed --trust unconditionally. The flag is
build/version-dependent (headless-only, and absent from some installed
cursor-agent binaries), so CLIs that don't recognize it exit 1 with
"unknown option '--trust'", breaking the Test button and task execution.
Declare helpArgs + capabilityFlags so detection probes --help and
buildArgs only appends --trust when the installed CLI advertises it,
matching the existing claude/codebuddy capability-gating pattern.
Fixes#4461
On Corepack-only setups (e.g. native Windows PowerShell where `corepack
pnpm` works but no standalone `pnpm` is installed on PATH), nested
package-manager invocations from tools-dev/tools-pack failed: the
no-`npm_execpath` fallback in createPackageManagerInvocation assumed a
global `pnpm` binary. Route those invocations through `corepack pnpm …`
instead — it runs the repo-pinned package manager out of the box, while a
bare `pnpm` only resolves with a separate global install or a
`corepack enable` shim. Applies to both POSIX and Windows.
Fixes#2438
* fix frontmatter parser coercing leading-zero values to numbers
The minimal YAML frontmatter parser (and its plugin-runtime mirror)
coerced any `-?\d+` token to a Number, so a leading-zero value like
`01234` (zip code, zero-padded id) silently became `1234`, and `007`
became `7`. Per YAML 1.2 a leading-zero decimal is a string — the zero is
significant. Restrict integer/float coercion to values without a redundant
leading zero (`0` alone still coerces to the number 0).
Also fix a related edge case where a value that is a single quote character
(`"`) was treated as an empty quoted string (slice(1,-1) of a 1-char
string), now requiring at least two characters before stripping quotes.
Both parser copies are kept in sync per their "mirror" contract.
Adds tests/frontmatter.test.ts for the daemon parser and extends the
existing plugin-runtime parsers test.
* fix(frontmatter): coerce leading-zero numerics per YAML 1.2 core schema
Restore numeric coercion for leading-zero scalars (01234, 007, -01, 00.5)
to match the YAML 1.2 core-schema int/float resolution, per review. The
lone-quote guard (require length >= 2 before stripping matching quotes)
stays in place. Tests updated to assert the numeric results.
Applies to both the daemon parser and the plugin-runtime mirror.
---------
Co-authored-by: Nagendhra <nagendhra405@gmail.com>
* feat(landing): add "What is vibe design" blog post
New /blog/what-is-vibe-design pillar targeting the "vibe design" keyword
cluster (definitional/informational intent), fully localized across all 18
landing locales.
Also adds two backward-compatible blog primitives (existing posts unchanged):
- optional team-persona author byline (avatar + name + role) via a new
blog-authors registry and an optional `author` frontmatter field;
- optional `ctaKind: download-app` frontmatter that points the post's
conversion CTA at the desktop client download page (Discord stays secondary).
Illustrations are original editorial concept art (no third-party screenshots).
* fix(landing): register vibe-design cover in blog index postImages
The post committed /blog/what-is-vibe-design-cover.webp but the blog
index postImages registry was not updated, so the index card and
featured panel fell back to the generic media placeholder instead of
the editorial cover. Add the what-is-vibe-design entry so the index and
detail page share the same cover art.
---------
Co-authored-by: Joey <276262049+xne998808-ai@users.noreply.github.com>
* landing: skill-spec restyle, upstream parity, newsletter + nav copy updates
Landing-page-only rebuild of the home-refresh branch on top of current
main (the previous branch carried unrelated apps/web changes and
conflicted with main):
- Homepage: newsletter band between CTA and FAQ (localized in all 18
locales, client-side stub until a provider lands), AMR logo in the
nav, hero subtitle now reads 'best open-source alternative' in every
non-English locale, hero/nav download labels swapped.
- /plugins/: upstream hub structure (3 tiles); /plugins/templates/ +
kind pages: breadcrumb and counter chip removed, scene chip
active-state contrast fix, tpl-card styles with token colors.
- /agents/: design-spec pass (token spacing incl. 68px max module gap,
font scale, 8px radius), 3-up card grid, list-indent fix; /solutions/,
/agents/, /plugins/ unified on one 1180px centered safe area.
- /blog/: cream/terracotta leftovers replaced with token colors, 8px
card radius.
- Header dropdowns: full 21-agent list, upstream caret/gap parity;
neutralized the homepage section padding leak inside info pages.
- package.json stays on main's shape; cobe / matter-js / @types/
matter-js added as exact pins (homepage globe + FallingText) with
matching pnpm-lock entries. nix/pnpm-deps.nix hash NOT refreshed
(no nix locally) — needs the Validate workspace gate or a follow-up.
* landing: fix CI gates while keeping the homepage refresh intact
Rebased onto current main and resolved the blocking review + red checks on
this branch without dropping the design refresh:
- Zero-client-JS gate: the cobe globe and matter-js FallingText enhancers
used ES `import`, so Astro emitted external `/_astro/*.js` module bundles
and tripped `Verify zero external JavaScript`. Both effects are preserved:
index.astro now reads each library's prebuilt runtime from node_modules at
build time and inlines it verbatim as a classic `<script is:inline>`
(cobe's single ESM default export is rebound to `window.__cobe`; matter-js
ships UMD), mirroring locale-switcher-script.astro. No bundled/module JS.
- Staging/preview noindex: restored the `staging-noindex-headers` Astro
integration that appends `X-Robots-Tag: noindex, nofollow` to `_headers`
for OD_LANDING_NOINDEX builds, so PR previews stay out of search indexes.
- Blob guard: removed 14 stray .bak/.prev/.leftpad backup files and 9
unreferenced PNG originals, converted 12 referenced >1MB PNGs to WebP
(each <1MB) with their refs updated, and recompressed lab-video.mp4 /
lab-hyperframes.mp4 under 1MB. No changed tracked file now exceeds 1MB.
- Residual-JS guard: allowlisted public/community/_site-nav.js, a verbatim
browser script for the static /community/ pages (same precedent as the
web notifications service worker).
- Homepage-art gate: the refresh serves its hero/gallery/method art as
optimized origin-hosted WebP instead of Cloudflare Image Resizing variants,
so the homepage no longer emits cdn-cgi URLs. The gate now requires the
same floor of 16 optimized WebP references (ci/staging/production) so a
regression to raw, unoptimized art is still caught.
* landing: stop 18x-localizing the skills/systems/templates catalog
The PR preview deploy hit Cloudflare Pages' 20,000-file-per-deployment cap:
the branch added `[locale]/{skills,systems,templates}` detail/listing
generators plus per-locale plugin previews, all fanned out across the 18
LANDING_LOCALES, producing ~29,300 output files.
This is exactly the locale-explosion the branch's own `public/_redirects`
already anticipated — its "Catalog migration" section says the standalone
skills/systems/templates generators "were removed" and are 301'd to
`/plugins/*`, but the generator routes were left in the tree, and the
localized 301 rules only covered 4 of the 18 locales.
Fix, matching that stated intent and Astro's guidance for bounded static
multilingual builds (prune the prerender matrix; signal i18n via
hreflang/sitemap, which this site already does — non-default locales are
sitemap-excluded):
- Remove the localized `[locale]/{skills,systems,templates}` detail, index
and facet generators, and the localized `[locale]/plugins/previews`
duplicate (previews are language-agnostic visual assets).
- Extend the localized catalog 301s in `public/_redirects` from {zh,zh-tw,
ja,ko} to every prefixed locale, and add `/<loc>/plugins/previews/*` →
the canonical English previews. Kept-localized plugin hub/detail pages
whose links degrade to these paths now resolve via a single 301 hop.
English catalog pages, all localized marketing pages, the localized plugins
hub, and the homepage design are unchanged. Output drops to ~19.2k files,
back under the deploy cap.
* landing: canonicalize legacy locale aliases in the header language switcher
The shared sub-page header language switcher builds its targets with
`localePath(target, Astro.url.pathname)`, and `stripLocaleFromPath` only
recognized canonical `LANDING_LOCALES`. The catch-all route still emits
legacy / alias locale pages (`app/_lib/i18n.ts` LOCALES: `/zh-CN/`, `/es-ES/`,
`/fa/`, `/hu/`, `/th/`, plus upper-cased `zh-TW`/`pt-BR`), so on those pages
the switcher produced doubled, non-existent targets like `/zh/zh-CN/blog/`.
Strip those alias prefixes too (case-insensitive, canonicalizing to the
default locale) so switching language from `/zh-CN/blog/` now lands on an
existing URL (`/blog/`, `/zh/blog/`, …). Verified against the static build:
the doubled `/<loc>/{zh-CN,es-ES,fa,hu,th}/…` targets are gone (0 matches),
and canonical `LANDING_LOCALES` pages are unchanged.
* landing: optimize homepage first-screen — fonts → subset woff2, hero → responsive webp
Resource-weight pass on the homepage, focused on the first screen.
Fonts (≈861 KB → ≈106 KB, and no longer render-blocking):
- Albert Sans regular/italic shipped as TTF (125 KB + 138 KB). Added woff2
builds (50 KB + 55 KB) as the primary `src`, TTF kept only as a fallback.
- `remixicon.ttf` was a 598 KB icon font loaded `font-display: block` (it hid
the icons — and text in the same paint — until the whole font downloaded).
The site only renders 11 of its glyphs, so it's now subset to a 1.3 KB woff2.
- All faces are `font-display: swap` now, and the above-the-fold Albert Sans
woff2 is `<link rel=preload>`ed at high priority, so text paints immediately
in the fallback and swaps without a blocking round-trip.
Hero imagery (responsive WebP instead of one oversized file for all devices):
- The hero backdrop was a single 326 KB / 2880px PNG served to every viewport.
Now responsive q90 WebP variants (960→33 KB, 1440→47 KB, 1920→89 KB,
2880→110 KB); `og:image`/`twitter:image` keep the PNG since social crawlers
don't reliably accept WebP. The LCP preload points at the WebP set with
`imagesizes="100vw"` to match the full-bleed `<img>`.
- The hero product shot now has a responsive srcset (800/1280/1920/2508);
the 2508px master is the pristine original (no re-encode), phones pull the
800px (66 KB) variant.
Quality is kept high (q90) — the savings come from format + right-sizing per
viewport, not heavier compression. Build output is unchanged structurally
(zero external/module JS still verified; ~19.2k files, under the deploy cap).
* landing: include all 25 used Remix Icon glyphs in the subset
The first subset only covered the 11 codepoints declared via CSS `content:`
and missed the 15 declared as JS \u escapes in page.tsx's `RI` glyph map
(download \uec5a, github \uedcb, star \uf18b, the arrow/chevron set, …), so
the hero download / Star CTA icons and several nav arrows rendered blank.
Re-subset to the full union of 25 glyphs (still ~2KB woff2).
* landing: add the locale-switcher caret glyph to the Remix Icon subset
A full scan of the built site (18,912 HTML files, every rendered Private-Use
codepoint) surfaced one more glyph the 25-glyph subset was missing: U+EA4E
(arrow-down-s-line), the language-switcher dropdown caret in header.tsx — it's
inserted as a literal char (not via the RI map), so the source grep missed it.
Re-subset to 26 glyphs (≈2.1KB); the same scan now reports zero missing
glyphs site-wide.
* landing: convert remaining raster art to WebP site-wide
After a site-wide image audit, converted the last referenced PNG/JPG raster
art to WebP (q88, no visible quality loss — the source files are lossless so
there's no generation loss):
- `lab-stage-bg` (homepage lab section): 710KB JPG → 323KB WebP.
- Blog study plates 09–12 and 16–22 (the hero/inline art in the Figma- and
Claude-alternative posts, referenced across every localized section):
~4.9MB of 1024² PNGs → ~2.6MB WebP. All `<img src>` references in the two
blog markdown files and the blog index were repointed; the originals are
removed.
Combined with the earlier hero/font pass, the homepage and blog now ship only
WebP raster art (PNG kept solely for the social `og:image`). Verified: build
clean, no dangling references to the removed files, zero external/module JS,
~19.2k output files (under the deploy cap).
* landing: make homepage og:image / twitter:image absolute URLs
`heroImage` is a same-origin path (`/hero-home.png?v=2`); social crawlers
(Facebook, Twitter, LinkedIn) need an absolute URL for rich link previews.
Resolve it against `Astro.site` at the metadata call site so the share card
renders as `https://open-design.ai/hero-home.png?v=2`. Per @PerishCode's
review note; non-blocking but correct.
* landing: absolute share image on sub-pages + canonical-locale plugin search.json
Two non-blocking output regressions from @PerishCode's review:
- sub-page-layout.astro wrote `og:image`/`twitter:image` from the raw
`ogImage ?? heroImage` (origin-relative), so every sub-page's share card
(plugins, agents, download, …) emitted `content="/hero-home.png?v=2"`.
Resolve against `Astro.site` like the homepage already does; `new URL()`
leaves an already-absolute `ogImage` untouched.
- [locale]/plugins/search.json.ts keyed off `_lib/i18n` PREFIXED_LOCALES
(the legacy alias table: zh-CN, es-ES, pt-BR …), so it emitted
`/zh-CN/plugins/search.json` whose entries linked to `/zh-CN/plugins/<slug>/`
detail pages that don't exist, while the canonical `/zh/plugins/search.json`
was missing. Drive it from `app/i18n` LANDING_LOCALES + localePath so each
endpoint matches the localized plugin detail routes and its hrefs resolve.
Verified: sub-page og:image is now absolute, /<canonical-locale>/plugins/
search.json exists with resolvable hrefs (legacy /zh-CN/ variants gone),
zero external/module JS, ~19.2k files.
* landing: preserve query + hash when switching language
The locale switcher's click handler only persisted the choice and let the
anchor's bare server-rendered href ('/<locale>/<path>/') drive navigation, so
a plain left-click dropped the current query string and hash — switching
language on '/plugins/templates/?kind=deck#gallery' landed on
'/<locale>/plugins/templates/'. Intercept the plain left-click and route
through the existing selectLocale(), which carries window.location.search +
hash across the switch via targetFor(); modified-click / non-left-button /
right-click still fall through to the real <a> (open-in-new-tab, etc.).
Per @PerishCode's review note. Verified: build clean, homepage still ships no
external/module JS, typecheck 0 errors.
* landing: handle legacy locale aliases in the switcher's path recompute
The previous commit routed plain locale-switch clicks through selectLocale(),
which recomputes the target via basePathFromCurrent(). That helper only treated
canonical LANDING_LOCALES as a strippable first segment, so on a legacy alias
page (/zh-CN/, /es-ES/, /fa/, /hu/, /th/ — still emitted by the catch-all)
clicking a language built a nested, non-existent URL like /zh/zh-CN/blog/.
Teach the switcher the same legacy-alias set as stripLocaleFromPath in
app/i18n.ts (zh-cn, es-es, fa, hu, th) via an isLocaleSegment() helper, so the
alias prefix is stripped before the target is rebuilt: /zh-CN/blog/ → ja now
yields /ja/blog/ (+ preserved query/hash), not /ja/zh-CN/blog/.
Per @PerishCode. Verified: build clean, switcher script carries the alias
handling, homepage still ships no external/module JS, typecheck 0 errors.
* landing: make plugin detail canonical-only + guard the deploy file count
The localized plugin detail wrappers (`[locale]/plugins/[...slug].astro` and
`[locale]/plugins/[slug]/index.astro`) regenerated the full ~450-plugin detail
set across all 17 non-default LANDING_LOCALES, so `build:static` emitted 19,234
files with `public/previews` empty — only ~766 below Cloudflare Pages' 20k-file
deploy cap, which the CI-generated preview thumbnails would erode further. This
is the same locale-explosion the PR set out to fix, left in the plugin subtree.
- Remove both localized plugin detail generators; plugin detail stays canonical
(English). Output drops 19,234 → 4,287 files (huge headroom).
- Add `/<loc>/plugins/* -> /plugins/:splat` 301s for every prefixed locale so
localized plugin links (still emitted by the kept localized hub pages) resolve
to the canonical detail page. Kept hub pages (/<loc>/plugins/,
/<loc>/plugins/{skills,systems,templates}/) have static files and win over
the redirect (static assets take precedence over _redirects on CF Pages).
- Add a post-build "deploy file count under Cloudflare Pages cap" step to the
ci / staging / production landing workflows (fail at >=19,000) so this limit
can never regress silently again.
Per @PerishCode. Verified: 4,287 files, localized hubs + canonical detail
intact, homepage zero external/module JS, typecheck 0 errors.
* landing: one canonical plugin detail route (drop namespace catch-all)
`plugins/[...slug].astro` keyed getStaticPaths off `plugin.slug`, which keeps
the registry namespace (`open-design/<name>`), so it emitted nested pages like
/plugins/open-design/<name>/ and self-canonicalized them via Astro.url.pathname.
But catalog cards and the search JSON link to `plugin.detailHref`
(/plugins/<last-segment>/), which the sibling `plugins/[slug]/index.astro`
already generates from `detailSlug`. The catch-all therefore produced ~440
orphan, indexable duplicate URLs that no surface links to — extra sitemap
entries and extra files against the Cloudflare Pages cap.
Delete the catch-all; `[slug]/index.astro` remains the single canonical detail
route at the exact `detailHref` URL the catalog/search already target. These
namespace URLs are net-new to this branch (never shipped/indexed), so no
redirect is needed. Output drops 4,287 → 3,848 files; canonical detail pages
(445) and the templates/[kind] hubs are intact.
Per @PerishCode. Verified: /plugins/open-design/* gone, canonical detail
renders, file-count guard passes, homepage zero external/module JS, typecheck
0 errors.
* landing: fix dead locale-prefixed links to canonical detail pages and migrated catalogs
Plugin/skill/template detail pages are canonical-only since the catalog
migration, but the card components still ran detailHref through
localizedHref, 404ing every card click on all 17 non-default locales
(430+ links per locale). The homepage lab dock, the design-systems stat
card, and the html-video CTA also still pointed at the legacy /skills
and /systems paths. Cards now link detail pages canonically; homepage
and html-video links target the /plugins/* catalogs and kind facets.
* landing: anchor the community dropdown into the hub sections again
Contributors / Ambassadors / Moderators in the header dropdown linked
straight to the standalone static pages, surfacing them on a plain
menu click. Restore upstream main's destinations — /community/#... hub
anchors — so the dropdown scrolls the hub; the standalone pages stay
reachable from the hub cards only.
* landing: giant two-tone wordmark sign-off under the footer columns
A masthead-style "Open Design." wordmark (ink + brand green via the
--ink / --coral tokens, Albert Sans 800, container-filling clamp size)
closes both footers — the homepage one in page.tsx and site-footer.astro
share the .foot-masthead class contract. Mobile gets extra bottom
padding so the wordmark clears the page-bottom GradualBlur band.
* landing: mobile responsive polish + nav liquid-glass caustic fixes
Mobile / responsive:
- nav menu panel: solid white surface (no backdrop blur), full capsule
width, and hide the page-level bottom GradualBlur band while open
- unify section rhythm to a consistent 112px gap (56px block padding)
across the home page and the info-page family (solutions / agents /
compare) at <=880px
- final CTA stacks as copy -> tag -> buttons -> image
- design-system cards: one per row on phones, and stop short brand names
wrapping mid-word
- template registry search: submit button only activates and the clear
(x) only appears once there is a query
- solution-step cards: even padding on all four sides (drop min-height +
row stretch)
- about-tab switch no longer scroll-jumps on mobile (the pin-scroll only
runs while the section is actually pinned on desktop)
Nav "liquid glass" caustic:
- center the displacement map: opaque (#000) gradient stops instead of
transparent (#0000), so the R/B channels ramp linearly and the neutral
point sits at 50% instead of ~29% (was biased to one side)
- halve the refraction displacement (feDisplacementMap scale -100/-94/-88
-> -50/-47/-44) so content behind the bar distorts less
Icon font:
- re-subset remixicon.subset.woff2 from the full TTF so every glyph stays
centered in its advance box (the old subset left-aligned outlines to
x=0, pushing icons like the Labs dock "video" tile off-center)
Note: globals.css, sub-pages.css and plugins/templates/index.astro also
carry in-progress working-copy edits that are interleaved with these
changes in the same blocks and could not be split out cleanly.
* landing: restore PostHog analytics + localize templates search
Address review on #4497:
- Restore <PostHogAnalytics /> in index.astro and sub-page-layout.astro;
the merge had dropped it, silently disabling download/share/locale/FAQ/
page_view tracking on every landing page (helper tests pass in isolation).
- Route the redesigned templates search widget labels through pcopy
(searchLabel/searchClear added to PluginsCopy + en/zh-CN/zh-TW); they were
hardcoded English on localized /<locale>/plugins/templates/ pages.
* landing: wrap the contributors heading on mobile
The testimonial heading keeps each <br>-split line on a single line via
`.testimonial-copy h2 { white-space: nowrap }` (+ the `.testi-post` span),
which is fine on the content-sized desktop two-column track but overflows a
phone — the heading was clipped at the right edge ("…329 co", "…toget").
On <=880px let both lines wrap (white-space: normal) and give the copy
column min-width:0 so the nowrap content can't blow the single grid track
wider than the viewport.
---------
Co-authored-by: wangchenglong <879618852@qq.com>
Co-authored-by: lefarcen <ontf116@gmail.com>
Co-authored-by: wangchenglong <honam884844@gmail.com>
audit of all official Twitter/X account entry points across the landing page:
- profile links (header / footer / home / community nav): x.com/nexudotio -> x.com/OpenDesignHQ (4)
- share-tweet @mentions (plugins/templates/skills, all locales): @opendesignai -> @OpenDesignHQ (55)
two handles were inconsistent (nexudotio vs opendesignai); now unified. links like x.com/compose
(share intent) and x.com/cursor_ai (Cursor reference in content) left untouched.
Co-authored-by: Joey <276262049+xne998808-ai@users.noreply.github.com>
* Consolidate PR validation into ci
* Tune absorbed CI runner tiers
* Fold visual validation into CI Playwright shards
* Run PR shards during manual CI dispatch
* Optimize CI topology and scopes
* Increase UI P0 Playwright workers
* Relax visual Playwright timeout
* Refresh Codex auth status after CLI changes
* Probe Claude Code auth status
* Stabilize project route during agent refresh
* Respect API key auth during CLI probes
The floating jump button is absolutely positioned at z-index 6 in its own stacking context, and the conversation-history dropdown is z-index 30 in a separate stack, so z-index can't keep the button behind it and it bleeds through the open dropdown. Gate the active class, aria-hidden, and tabIndex on !showConvList so the button hides while the dropdown is open and returns when it closes.
This was originally bundled in #4238; the other half of that PR (a workspace-tab tooltip change) is now obsolete after the entry topbar tooltip-unification, so this lands the still-valid fix on its own.
Fixes#4123
* test(e2e): red-spec reproducing #4303 design-system dropdown clipping
The New Project design-system popover (local DesignSystemPicker in
NewProjectPanel.tsx) opens with plain position:absolute and is clipped
by its .newproj-body { overflow-y: auto } ancestor, so lower options are
truncated. This P1 UI spec opens the picker on a short window and asserts
the popover does not spill past the .newproj-body / viewport bottom.
https://claude.ai/code/session_01EqWRnbYoxmugj4R1yj2LJ6
* fix(web): portal New Project design-system dropdown so it isn't clipped (#4303)
The New Project modal's design-system picker opened its popover with plain
position:absolute, anchored inside .ds-picker. Its clipping ancestor is
.newproj-body { overflow-y: auto } (the modal scroll container), so when the
trigger sat low in the body or the window was short, the popover was truncated
and the sections below bled through — the reported misaligned/truncated UI.
This is platform-independent (shared web CSS rendered by the same Chromium in
macOS and Windows desktop), not a macOS-only regression.
Mirror the shared DesignSystemPicker: render the open popover through a portal
to document.body, positioned from the trigger's viewport rect with
viewport-aware up/down placement and a clamped max-height, so it escapes the
clip box. A scoped .ds-picker-popover-portal variant flips the popover to
position:fixed (z-index above the modal backdrop) and lets the list flex and
scroll inside the bounded height. The sibling PlatformPicker shares the same
absolute-popover pattern and is left for a follow-up.
https://claude.ai/code/session_01EqWRnbYoxmugj4R1yj2LJ6
* fix(web): clamp design-system popover height to available viewport space
Review follow-up (#4346, thanks @mrcfps): the anchor used
Math.max(200, Math.min(440, space)), so when both sides of the trigger were
under the 200px preferred minimum (a very short window) the fixed popover was
still sized to 200px and overflowed the viewport instead of staying scrollable.
Size the popover to the chosen side's actual available space, honoring the
200px preferred minimum only when the side can fit it. The list already flexes
and scrolls inside the bounded height, so a smaller box just scrolls rather than
spilling past the viewport edge.
Extend e2e/ui/new-project-ds-picker-clipping.test.ts with a tight-viewport case
(1280x360) that asserts both sides fall under 200px and the popover stays within
the viewport.
https://claude.ai/code/session_01EqWRnbYoxmugj4R1yj2LJ6
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Show boot stage progress on the desktop splash screen
The splash video plays ~1.7s and then freezes on its final frame while
the packaged cold boot (daemon, web sidecar, Next.js standalone server,
web mount) can take many seconds on slow machines, reading as a hang.
Add a muted status line under the logo with continuously pulsing dots
and stage labels that advance through the real boot phases:
- Starting Open Design (baked into the splash HTML)
- Starting the local engine (daemon sidecar spawn)
- Preparing the interface (web sidecar spawn)
- Opening your workspace (hidden main window loading + mount gate)
Stage transitions follow the repo animation philosophy (140ms ease-out
exit, 200ms ease-out enter). The packaged entry forwards phases through
a new optional onPhase hook on startPackagedSidecars; the runtime
re-asserts the final stage at its reveal gate so the tools-dev splash
path is covered too. setSplashStage is safe on destroyed windows and
idempotent per label.
https://claude.ai/code/session_01MHt6dRc7GMAcu9Npo5Bgdx
* Guard splash stage updates against the page-load race
The first boot-stage update (the daemon phase) is fired from the
packaged entry immediately after the splash window is created, which
can be before the splash data-URL has finished loading and defined
window.__odSplashSetStage. The previous setSplashStage ran the update
unconditionally, so on a fast-spawning cold boot the renderer had no
setter yet and the 'Starting the local engine' label was silently
dropped — on exactly the slow-PC path this feature targets.
Track per-splash load readiness: registerSplashStageTracking arms a
did-finish-load listener before loadURL, and stage updates that arrive
before load are stashed (latest wins) and replayed once the page
reports it has loaded. Updates after load apply immediately as before.
setSplashStage / registerSplashStageTracking now take a narrow
SplashStageSurface so the replay logic is unit-testable without a live
Electron renderer; BrowserWindow satisfies it structurally.
Red spec: tests/main/splash-stage-replay.test.ts — fails when the
deferral is removed (update fires before load), green with the guard.
https://claude.ai/code/session_01MHt6dRc7GMAcu9Npo5Bgdx
* desktop(splash): granular boot stages with step counter and progress bar
The first-run cold boot on a slow machine sat on the brand splash with only
four coarse status labels and no sense of how far along it was, so the wait
read as a possible hang.
Make the boot progress finer-grained and quantified:
- Expand the splash stage set from 4 to 7, marking both the "starting X" and
"X ready" edges of the two long native waits (local engine online, web
interface online) plus a final "Almost ready" step, so the status visibly
advances right after each long wait clears.
- Show a "N/total" step counter and a thin progress bar under the logo that
fills as stages advance; the bar is monotonic so an idempotent re-asserted
stage cannot make it jump backwards.
- Emit the daemon-ready / web-ready edges from the packaged sidecar bring-up
via the onPhase hook so the new stages map to real boot transitions, and
assert the final "finishing" stage once the web bundle reports it mounted.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: 박세원 <s-w.park@samsung.com>
* fix(amr): surface vela login URL/code so sign-in survives a failed browser handoff
AMR (vela) sign-in is a device-authorization flow: `vela login` prints the
activation URL + user code to stdout, then best-effort opens the browser and
keeps polling until the server-issued expiry (>5min). The daemon spawned it
headless, only read the first 250ms of output, and the UI showed nothing but a
"Signing in…" spinner — so when the browser failed to open (stripped PATH in a
packaged build, blocked network) the user had no URL to fall back to and stared
at a dead spinner for the full AMR_LOGIN_TIMEOUT_MS (5 minutes).
- daemon: parseVelaLoginActivation + lifetime stdout/stderr capture surface
activationUrl / userCode / browserOpenFailed on VelaLoginStatus.
- web: the AMR account pill's signing-in state now shows an activation block
(open sign-in page + copy code) as soon as the URL is known, instead of only
a spinner; it adapts to the active theme.
- web: an AMR run that fails with AMR_AUTH_REQUIRED now offers inline sign-in in
the chat error card (the login pill) and auto-retries once on success, rather
than bouncing the user out to Settings.
Tests: red->green parser spec (validated against real vela login output),
AmrAccountControl activation-block specs, ChatPane inline-auth specs.
* test(amr): real-vela E2E for the login activation capture seam
Spawns an actual `vela login` child through the daemon's spawnVelaLogin and
asserts readVelaLoginStatus surfaces the real activation URL + code while the
login is in flight, then cancels (never approves, so no config is written and
any existing session is untouched). Covers the daemon capture wiring
(beginLoginActivationCapture -> readVelaLoginStatus) that the pure parser spec
can't reach. Auto-skips where the vela CLI isn't installed, so CI is unaffected.
* fix(amr): polish inline auth retry layout
* fix(daemon): try AMR login direct first, fall back to IPv4 proxy
loopback AMR API proxy (forced IPv4) to dodge broken `amr-api.open-design.ai`
edge paths for some hosts (#3726). But behind a corporate transparent proxy
that hijacks `amr-api.open-design.ai` onto an internal gateway (e.g.
飞连/CorpLink → 30.x), the extra proxy hop makes the upstream lose the client
IP and reject device authorization with
`502: Invalid IP address: undefined` — even though the direct path resolves
fine. Result: AMR "Authorize" fails in every packaged build that ships this
change (beta/main), while release builds without it still work.
Only `vela login` was rerouted; the model catalog and chat inference already
go direct (to amr-link / VELA_LINK_URL), so this restores login to the same
direct-first default. The IPv4 proxy is kept strictly as a fallback: the
route now starts login over a direct connection and only retries through the
proxy when the direct attempt fails to start — never when a login is already
in flight. Both cohorts win: transparent-proxy users succeed directly,
broken-edge users (#3726) still get the proxy.
Tests: fake-vela gains FAKE_VELA_LOGIN_FAIL_WITHOUT_API_URL to model a broken
direct path; the login suite now asserts direct-first (no VELA_API_URL on a
healthy path) and proxy-fallback (VELA_API_URL set only after a direct
failure, incl. OD_PUBLIC_BASE_URL derivation).
* fix(amr): clear stale login activation details
* fix(amr): refresh login status immediately after auth starts
* fix(amr): sync inline auth card with login status
* fix(amr): tighten inline auth retry state
* fix(amr): wait for direct login activation before fallback
* fix(amr): show activation details only in chat auth card
* fix(amr): guard clipboard copy explicitly in the activation block
navigator.clipboard?.writeText(...).then(...) already short-circuits the whole
chain when the clipboard API is absent, but make the guard explicit so the copy
handler reads as a clean no-op rather than relying on optional-chain reach.
* fix(amr): don't kill a slow-but-healthy direct login; only fall back on real failure
The login activation watchdog turned a merely slow direct CreateDeviceAuthorization
into a hard failure: after the grace it killed the direct child and let the route
retry through the IPv4 proxy — the exact proxy hop that 502s on the transparent-proxy
networks this fix targets. Now the grace only stops blocking the request; a still-
running direct attempt is left alive (the UI polls /status), and the proxy fallback
fires only on an explicit pre-activation exit/error. The grace is overridable via
OD_AMR_LOGIN_ACTIVATION_GRACE_MS so the new route test can exercise the slow-direct
path quickly (fake-vela gains FAKE_VELA_LOGIN_ACTIVATION_DELAY_MS).
Addresses review feedback on #4402.
* test(e2e): AMR auth failure now signs in inline, not via Settings
The chat AMR_AUTH_REQUIRED card replaced its 'open Settings to authorize' bounce
with an inline sign-in pill (POSTs vela login in place, auto-retries on success).
Update the [P0] runtime-recovery spec to assert the inline flow: clicking
Authorize & retry issues the login request and opens no Settings dialog. The
non-AMR 'Switch to AMR' recommendation flow (AmrGuidance) is unchanged.
* fix(amr): only auto-retry inline auth on a real sign-in transition
The inline AMR auth card auto-retried the failed run whenever /status reported
signed-in — via both the 500ms shared-status poll and the pill's onStatusChange.
Because the per-assistant-id guard resets on each retry (new run = new id), a run
that keeps failing AMR_AUTH_REQUIRED while /status reads signed-in retried
forever (caught by the runtime-recovery UI P0 spec). Now we retry only on an
explicit signed-out -> signed-in transition; a null/unknown status never
fabricates one, and once signed-in we don't retry again until an explicit
signed-out is seen.
- Unit specs updated to drive the transition (and a loop-guard spec for the
already-signed-in case).
- e2e: the AMR auth spec asserts the inline sign-in flow (no Settings bounce);
the switch-to-Codex spec mocks /status signed-out, the realistic state for an
AMR_AUTH_REQUIRED failure, so the Authorize & retry action is offered.
* fix(amr): fall back to the proxy on a post-grace direct login failure
The direct-first login route only fell back to the IPv4 proxy when the direct
child crashed inside the 250ms startup grace. A direct vela login that survives
the grace and then errors out before printing its activation URL (the common
broken-edge shape, #3726) returned 202 and stranded the user on a dead direct
login. Re-introduce waitForActivation: the route now blocks until the direct
attempt reaches device-auth steady state (prints its activation URL) or
exits/errors before it; only a pre-activation exit/error triggers the proxy
retry, and a slow-but-healthy direct login is left running (never killed).
fake-vela now prints the activation block (so the daemon can detect steady
state) and gains FAKE_VELA_LOGIN_FAIL_WITHOUT_API_URL_DELAY_MS; the route suite
gains a regression where the direct attempt fails past LOGIN_STARTUP_GRACE_MS
and must still reach the proxy. Addresses review feedback on #4402.
* Consolidate PR validation into ci
* Tune absorbed CI runner tiers
* Fold visual validation into CI Playwright shards
* Run PR shards during manual CI dispatch
* landing: mobile responsive fixes for home modules
Rebased onto current main; drops items main already shipped independently
(hero nav clearance, testimonial 238px safe area, mobile nav scroll,
hamburger toggle, live GitHub counts). Remaining unique fixes:
- testimonial: collapse copy/globe to a single column ≤1400px so the
headline isn't squeezed by the globe at mid widths
- capabilities: flatten head+row (display:contents) into a grid so the
steps lay out two-up with the DONE mark beside them on mobile
- coding-agent steps: switch the active frame by tap on mobile (the
scroll-driven reel left every frame stacked when unpinned)
- CTA: put the product window in flow below the buttons with a responsive
gap so they never overlap (was a fixed 46% that collided with the
two-line heading)
- footer: lay link sections out as horizontal rows on mobile
* landing: move DONE mark out of <h2> so the capabilities grid binds
The capabilities-head CSS (desktop flex rules at globals.css:2222-2224 and
the mobile grid at 2407-2415) was authored expecting .cap-head-icon to be a
direct child of .capabilities-head. In the markup it was still nested inside
the <h2>, so display:contents flattened only .capabilities-head/.cap-row but
never the heading — grid-area:icon never applied and the DONE mark stayed in
the title row on mobile. Promoting the <img> to a sibling of the <h2> makes
both layouts bind as authored: a flex item beside the heading on desktop
(20px column-gap, margin-left:0) and a grid item beside the two-up steps on
mobile. Verified at 414px: grid-area resolves to icon and the mark renders to
the right of the 01/02 + 03/04 steps.
---------
Co-authored-by: lefarcen <935902669@qq.com>
* fix(web): show sending state on home composer submit
The Home composer's Send click kicks off an async project/conversation
creation roundtrip, but the button stayed in its idle state until
navigation unmounted the screen — no spinner, no disabled feedback, and
repeat clicks could fire duplicate creations (#4082).
EntryShell now forwards onCreateProject's result through the submit
handler, HomeView holds a sending flag for the whole async tail (a
pending plugin apply and the creation roundtrip — both re-entrable
windows before this change), and HomeHero renders it: the send button
disables itself, swaps its label to the existing chat.comments.sending
string, and folds the flag into canSubmit so the Enter-to-send path is
gated by the same window. A rejected creation — or a submit handler
that throws — re-enables the composer with a visible error and keeps
the staged contexts so the retry sends the same payload.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): defer spending the example-prompt marker until create is accepted
A rejected create keeps the Home composer retryable, but the one-shot
od:example-prompt-used marker was written while building the onSubmit
payload — before the create's accepted result was known. A failed
attempt therefore spent the marker, so the retry dropped
examplePromptContext and the user lost the example flow they selected.
Decide whether to send examplePromptContext before the await, but only
localStorage.setItem the marker after accepted !== false; reject/throw
paths leave it unspent so the retry resends it. Adds a regression spec.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(daemon): normalize stale service_tier=priority in ~/.codex/config.toml before Codex launch
Codex CLI renamed service_tier="priority" to "fast" in a recent release.
The Codex app's own fast-mode toggle still writes the old value on some
installations, causing the CLI to exit with:
Error loading config.toml: unknown variant 'priority', expected 'fast'
or 'flex' in `service_tier`
The CLI parses config.toml before processing any -c overrides, so the
only fix is to patch the file on disk. Before spawning the Codex agent,
the daemon now calls normalizeCodexConfigFile() which replaces any stale
service_tier="priority" with service_tier="fast" in-place, leaving all
other config content intact. Errors (missing file, permission denied) are
silently swallowed so a read-only or absent config.toml never blocks
a launch.
fixes#4276
* fix(codex-config-normalize): line-anchor regex + atomic write + logged errors
Fixes two code-review blockers on the #4276 normalization module:
BLOCKER 1 — regex not line-anchored (data corruption):
Replace the global /\bservice_tier\s*=.../ pattern with a multiline-anchored
pattern /^(\s*)service_tier(\s*=\s*)(["'])priority\3(\s*(?:#.*)?)$/gm.
The new pattern only matches a standalone key line at the start of a line,
so "priority" appearing inside another key's value or inside a comment is
never rewritten. Indentation, original = spacing, and trailing inline comments
are preserved via capture groups.
BLOCKER 2 — unsafe in-place overwrite + swallowed errors:
Replace direct writeFile(configPath) with atomic temp-file + rename:
writeFile(configPath.<hex>.tmp) → rename(tmp, configPath)
Same-directory temp ensures same filesystem so rename is atomic. Write/rename
failures are now logged via console.warn (matching the daemon pattern) instead
of a bare catch {}. An injectable CodexConfigIO interface is exported so tests
can stub the rename to exercise the failure path without filesystem tricks.
Temp file cleanup is attempted on failure; secondary cleanup errors are ignored.
Tests: 11 new regression cases added to codex-config-normalize.test.ts:
- BLOCKER 1: priority in unrelated value/comment NOT rewritten; trailing
inline comment preserved; indentation preserved; flex/fast untouched;
real key coerced even when priority appears elsewhere in the file.
- BLOCKER 2: no temp-file litter on success; full content correct after
atomic write; rename failure logged and does not throw; unlink attempted.
* fix(codex): pass merged spawn env to normalizeCodexConfigFile (#4276)
The call site in server.ts was passing bare `process.env` to
`normalizeCodexConfigFile`. If a user configured CODEX_HOME via
Open Design's agentCliEnv.codex, the normalizer resolved the wrong
path (~/.codex/config.toml) and left the real config at the user's
CODEX_HOME with a stale service_tier="priority" value untouched.
Fix: pass the same merged env the Codex child launches with —
`{...process.env, ...(def.env||{}), ...configuredAgentEnv}` — so
resolveCodexConfigPath sees the same CODEX_HOME the child will see.
Also adds:
- FIX 1 regression tests: positive case (CODEX_HOME in merged env is
used) and negative case (without CODEX_HOME, default path is used)
- FIX 2 CRLF regression test: config.toml with CRLF line endings has
service_tier coerced to "fast" with all \r\n sequences preserved
FIX 3 (logging): console.warn at codex-config-normalize.ts:138 is
left as-is — no structured logger is trivially in scope at the call
site without extending the CodexConfigIO injection pattern, and the
reviewer confirmed console.warn already matches daemon conventions.
* fix(codex): expand tilde in CODEX_HOME before normalizing config path (#4276)
resolveCodexConfigPath now calls expandHomePath on the CODEX_HOME env value
before passing it to path.join. Without this, a user-configured
CODEX_HOME="~/.codex-alt" resolved to the literal path
"~/.codex-alt/config.toml" in the normalizer while the Codex child process
(via spawnEnvForAgent → expandConfiguredEnv) resolved it to the absolute
"<homedir>/.codex-alt/config.toml". The stale service_tier="priority" config
at the real expanded path was therefore never found and never patched.
Also routes the server.ts normalizer call through spawnEnvForAgent('codex', …)
instead of a hand-rolled env merge, mirroring the diagnostics-export.ts
`envFor('codex')` pattern to permanently eliminate drift between the env the
normalizer sees and the env the Codex child actually receives.
Adds two regression tests for the tilde case (RED before this fix, GREEN
after): one unit-level assertion that resolveCodexConfigPath expands "~/" to
an absolute path, and one end-to-end assertion that a stale config at the
expanded location is patched when CODEX_HOME contains a tilde prefix.
Refs #4276