Commit Graph
1094 Commits
Author SHA1 Message Date
YOMXXXandGitHub 7a118424b6 feat(mcp): add write_file, delete_file, delete_project tools (#2416)
* feat(mcp): add write_file, delete_file, delete_project tools

External coding agents driving Open Design through MCP can create new
artifacts (create_artifact) but cannot iterate on a file once written
(create_artifact rejects existing targets), cannot remove a stale
file, and cannot tear down a throwaway project they just spun up via
create_project. Close that loop so the same agent can drive the full
file/project lifecycle end-to-end through MCP.

- write_file(path, content, encoding?): POSTs to /api/projects/:id/files
  without `artifact: true`, which the daemon route writes as a plain
  overwrite. Supports nested paths and base64 binaries.
- delete_file(path): DELETEs /api/projects/:id/raw/<path> so nested
  paths work just like create_artifact's nested name argument.
- delete_project(project, confirm:true): DELETEs /api/projects/:id but
  refuses to fall back to the active project and requires confirm:true,
  since the operation purges the SQLite row and on-disk project dir
  irreversibly. Marked destructiveHint:true on the annotation.

Tests cover each tool's success path, the active-context fallback for
write/delete_file, missing-argument rejection before any network call,
the daemon-error mapper, and the two delete_project guards.

* fix(mcp): echo resolvedProject from delete_project and cover the daemon error path

Two follow-ups from review of #2416:

- delete_project accepts a name substring per its inputSchema and the
  server instructions block tells callers to verify which row was
  matched via resolvedProject. write_file/delete_file already honor
  that contract via withActiveEcho(json, active, resolved), but
  deleteProject destructured only `id` and dropped the echo on the
  one irreversible tool. Capture `resolved` and pass it through;
  active is always null here because the active-context fallback is
  intentionally disabled.
- formatDaemonError and the !resp.ok branches in writeFile/deleteFile/
  deleteProject had zero coverage — all nine tests stubbed status: 200.
  Add three regressions covering the structured-error reformat, the
  raw-text fallthrough for non-JSON bodies, and the irreversible
  delete_project surface, so a regression in the parse/fallthrough
  logic will fail in CI instead of reaching agents.
2026-05-23 00:31:04 +08:00
Marc ChanandGitHub 1c7233ef10 fix(landing-page): speed up landing-page CI builds (#2734)
* fix(landing-page): speed up landing-page CI builds

* fix(landing-page): disable dev-only landing caches

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

* fix(landing-page): reuse previews across CI runs

* fix(landing-page): hash shared preview dependencies

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

* fix(landing-page): skip missing preview html reads

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

* fix(landing-page): rerun previews on cache hits

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

* fix(ci): repair landing-page workflow cache keys
2026-05-23 00:30:31 +08:00
Marc ChanandGitHub a3872b97a9 fix(tools-dev): preserve web origin trust on web start (#2715)
* fix(tools-dev): preserve web origin trust on web start

Restart daemon/web when the trusted web port is missing, and reuse the active web port during repeated starts so run web and start web keep app-config origin checks aligned.

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

* fix(plugins): refresh official registry bundled count

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

* fix(tools-dev): preserve daemon/web reserved ports

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

* fix(tools-dev): preserve daemon reuse on web start

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

* fix(tools-dev): preserve running daemon port on web reuse

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

* fix(tools-dev): reserve explicit web port before daemon allocation

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

* test(web): stabilize media provider reload flash timing

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

* fix(web): restore merged reattach workspace coverage

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

* fix(tools-dev): reserve allocated daemon port

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

* test(e2e): wait for artifact manifest persistence

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)
2026-05-23 00:25:43 +08:00
soulmeandGitHub b24168b340 fix conversation title editing (#1926) 2026-05-23 00:25:05 +08:00
ce2e7a0e66 fix(web): chat pane preserves scroll position when todo card grows (#2299)
* fix(web): chat pane preserves scroll position when todo card grows

The PinnedTodoSlot renders outside the chat-log scroll container. When
the todo card grows (new tasks added via TodoWrite), the scroll container's
clientHeight shrinks in the flex layout, drifting the user away from the
bottom. The existing ResizeObserver only observed children of the chat-log
div, so pinned-todo growth was invisible to followLatestIfPinned.

Fix: pass a containerRef to PinnedTodoSlot and observe that element in the
same ResizeObserver. syncPinnedTodo() is called on effect setup and from
the MutationObserver callback so observation stays current as the slot
appears and disappears across TodoWrite snapshots.

Red spec: apps/web/tests/components/chat-todo-autoscroll.test.tsx

* fixup! fix(web): chat pane preserves scroll position when todo card grows

Clarify test comment: the second test confirms followLatestIfPinned
snaps scroll to bottom when fired. The structural guarantee (pinned-todo
element is observed) is separately asserted in test 1, which is the
check that goes red on main without the fix.

* fix(web): correctness extend MutationObserver to pane ancestor for PinnedTodoSlot mount detection

The MutationObserver was only watching the .chat-log element. PinnedTodoSlot
(.chat-pinned-todo) is a sibling of .chat-log-wrap inside .pane, outside the
observed subtree. syncPinnedTodo inside the MutationObserver callback was
therefore dead code for mount/unmount transitions of the slot.

Add a second observation on paneEl (el.parentElement?.parentElement) with
childList-only so the MutationObserver fires when PinnedTodoSlot mounts or
unmounts and syncPinnedTodo can register/deregister the element with the
ResizeObserver.

* test(e2e): chat pane auto-scroll on todo card growth

Add Playwright spec that goes red on origin/main and green on this fix
branch. Scenario A asserts that a chat-log pinned to the bottom snaps
back after the PinnedTodoCard grows (the ResizeObserver-on-pinned-todo
path). Scenario B asserts that a deliberate scroll-up is not overridden.

Also allow OD_WORKSPACE_ROOT env override in next.config.ts so Turbopack
resolves node_modules correctly when the web app is booted from a worktree
whose node_modules symlinks resolve outside the default workspace root.

* docs(agents): note pinned-todo observer coverage in chat UI conventions

PinnedTodoSlot sits outside the .chat-log scroll container, so the
ResizeObserver and MutationObserver coverage that keeps auto-scroll
working when the todo card grows is non-obvious to future implementers.
Document the invariant in the Chat UI conventions section.

* fix(web): validate OD_WORKSPACE_ROOT, harden autoscroll test precondition

* fix(web): validate OD_WORKSPACE_ROOT existence, make autoscroll precondition unconditional

* fix(web): throw on invalid OD_WORKSPACE_ROOT instead of warn-and-fallback

* fix(web): require pnpm-workspace.yaml at OD_WORKSPACE_ROOT, drop dead test branch

Three follow-ups to nettee's review feedback:

1. apps/web/next.config.ts gains a pnpm-workspace.yaml existence check
   after the relative-path validation. Without it, an override like
   '<repo>/apps' or '<repo>/apps/web' passes the relative(resolved, WEB_ROOT)
   check but the resolved path is missing the sibling packages/* directory
   that apps/web imports from (for example @open-design/contracts). Next
   would later fail deep inside file tracing / Turbopack with a much
   harder-to-diagnose error. Now we throw at config load with a clear message.

2. e2e/ui/chat-todo-autoscroll.test.ts drops the redundant
   'if (scrollUpOccurred)' branch. The hard precondition above it already
   guarantees distanceAfterScroll > 80, so the if was dead code that read
   as a false-green path. The body now runs unconditionally.

3. Same test tightens the post-grow assertion. The previous
   toBeGreaterThan(60) would pass even if a regression dragged the log
   most of the way back to the bottom (e.g. before=150, after=61).
   Replaced with Math.abs(distanceAfterGrow - distanceAfterScroll) less than
   SCROLL_PRESERVATION_TOLERANCE_PX (20) — a delta check that actually
   verifies the comment's claim of 'within ~20px of where the user left it'.

* fix(web): canonicalize workspace root with realpathSync and tighten scenario B assertion

- Use realpathSync on both resolved and WEB_ROOT before the ancestor check so
  that symlinked paths (macOS /tmp vs /private/tmp, worktree checkouts) compare
  correctly instead of false-throwing on a physically valid override.
- Add isAbsolute(rel) guard for the Windows cross-drive case where path.relative()
  returns an absolute path instead of a ..-prefixed string.
- Scenario B: replace distance-to-bottom delta assertion with scrollTop preservation
  check. Growing the pinned todo naturally increases distance-to-bottom by ~extraPx
  (clientHeight shrinks while scrollTop is held fixed), so the old Math.abs(after -
  before) < 20 check would fail on correct behavior. asserting scrollTop directly
  catches the real regression: followLatestIfPinned incorrectly snapping a non-pinned
  user back to the bottom.
- Add hard precondition that clientHeight actually changed so the test fails fast
  if the layout stops exercising the non-pinned path.

* test(e2e,web): add clientHeight guard to scenario A and mount-wiring unit test

---------

Co-authored-by: Patrick A <eefynet@users.noreply.github.com>
Co-authored-by: Patrick A <259201958+eefynet@users.noreply.github.com>
2026-05-23 00:19:59 +08:00
shangxinyu1andGitHub ac0a9212fe fix(web): restore HTML preview after source toggle (#2699) (#2710) 2026-05-22 22:51:38 +08:00
f2703c8cb3 blog(landing-page): announce Open Design 0.8.0 plugin engine rebuild (#2731)
* blog(landing-page): announce Open Design 0.8.0 plugin engine rebuild

Adds the Product (announcement) post for the open-design-v0.8.0 release
(305 PRs · 75 contributors · 7 days; tag c20d156). Covers the three
architectural shifts that ship in 0.8.0 — plugin engine, headless-by-
default CLI, plugins create plugins — plus the design-system /
critique-theater / media-provider / packaged-auto-update plate. CTA
links to the GitHub release.

Also:
- Hero plate plate-15-plugin-engine-modular.png wired into postImages
  map in app/pages/blog/index.astro (editorial study plate, warm paper
  + red circle, per the open-design-blog-factory hero policy)
- _topics.md backlog updated: new row in Shipped, supersedes the queued
  #9 "auto-update on Windows + Linux" row (folded into this release
  announcement), Last reviewed bumped to 2026-05-22

Verified locally:
- pnpm typecheck — 0 errors, 0 warnings
- tsx scripts/blog-indexing/lint-blog-seo.ts — 0 errors, 0 warnings

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

* blog(landing-page): rework plate-15 to match editorial study-plate family

The first generation of plate-15 read as a marketing cover (16:9 frame
with baked-in "Plugin Engine Rebuild 0.8.0" headline + OPEN DESIGN
wordmark), which broke the visual register of plates 09–14 — all square
1024×1024 editorial study plates with a single dominant red circle and
no on-image typography.

Regenerated the plate to match the family: 1024×1024, warm cream paper
with deckle edges, a bold red circle in the upper-right, schematic
plugin tiles around a central black cube engine core, olive branch +
ink arc at the bottom, no headline / wordmark text. Updated the alt
text to describe the actual artefact.

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

---------

Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 21:39:59 +08:00
829fc01c1c feat(landing-page): detail pages — interactive preview, share row, dual CTAs (#2679)
* feat(landing-page): detail pages — interactive preview, share row, dual CTAs

Joey requested three additions to every `/skills/<slug>/` and
`/templates/<slug>/` detail page, with opendesigner.io's skills
catalog and youmind.com's seedance prompt page as references.

What
- **Interactive preview**: a `<details>` toggle below the static thumb
  reveals an `<iframe sandbox>` rendering the canonical artifact
  (`/skills/<slug>/example.html` for skill-template origins,
  `/templates/<slug>/template.html` for live-artifact origins). The
  iframe loads lazily — only on first toggle — so the page stays fast.
  An "Open in new tab ↗" pill on top-right of the frame links to the
  same URL standalone.
- **Six-channel share row**: Reddit, X, LinkedIn, Facebook, Email,
  Copy-link. Each anchor is a vendor "intent" URL (no tracker SDKs);
  the copy-link button uses the Clipboard API with a `prompt()`
  fallback for older Safari / embedded webviews. Wired by a small
  handler appended to `header-enhancer.astro`.
- **Two primary CTAs** in the detail-actions row:
  - "Use this skill →" / "Use this template →" routes to
    `/quickstart/?skill=<slug>` (or `?template=<slug>`). The OD
    desktop client has no public protocol handler yet, so a
    `od://skill/<slug>` deep link would 404. Quickstart is the v1
    pivot; once the client registers a scheme, the anchor flips to
    a JS try-`od://`-then-fallback without changing the page surface.
  - "Find on GitHub →" deep-links into the source folder.

Share copy keeps "open-source Claude Design alternative" front and
center across every channel — same brand keyword Google associates
with the homepage and `/alternatives/claude-design/`, so each social
click reinforces the same entity claim. Per-skill name + summary
follow so a reader who lands on a friend's tweet has a concrete
reason to click.

  - X intent: "I'm using <skill> from @opendesignai — the open-source
    Claude Design alternative.\n\n<description>"
  - Reddit submit title: "<skill> — open-source Claude Design alternative"
  - Email subject: same as Reddit; body: "I thought you'd like this —
    <skill>, an open-source Claude Design alternative skill from Open
    Design.\n\n<description>\n\n<url>"
  - LinkedIn / Facebook: URL-only (those vendors auto-fetch OG meta,
    so they read the existing canonical title + image).

Surface area
- Marketing site only. `apps/landing-page/app/pages/skills/[slug].astro`,
  `pages/templates/[slug].astro`, `_components/header-enhancer.astro`,
  `sub-pages.css`.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI surfaces.
- No new top-level dependencies. WeChat QR was dropped from the v1
  scoping in favor of Joey's revised channel set; brings Reddit and
  Facebook in instead.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: `/skills/deck-swiss-international/` shows all six share
  buttons, both CTAs, and the iframe `<details>` toggle. Same on
  `/templates/magazine-poster/`.
- Local dev: Reddit submit URL contains the SEO keyword in the title
  param; X intent URL contains the @opendesignai mention + keyword
  in the tweet body; Email mailto: subject + body wired correctly.

Followups
- Once OD desktop client registers a `od://` scheme, flip the "Use
  this skill" anchor to JS-driven try + fallback so installed users
  bypass /quickstart/.
- Translate the share copy + CTA labels across the 18 landing
  locales (currently English-only).
- `i18n.ts` `ui.catalog.skills` keys could absorb the share-copy
  template if we want per-locale share text in the future.

* fix(landing-page): preview clicks the thumb; CTA goes to releases

Two follow-ups to #2679 against Joey's review.

1. Preview UX: the thumb is the trigger
   The previous shape rendered a static thumb followed by a separate
   "View interactive preview ▸" disclosure row underneath. Joey wanted
   one composed unit: click the thumb itself to open the live frame.
   Wraps the existing `<details>` so that `<summary>` IS the thumb
   image (with a hover overlay revealing "Click for live preview ↗"),
   and once open the summary hides so the iframe lands in the same
   visual slot. The figcaption moves below the open/closed unit so it
   labels both states identically.

2. "Use this skill" / "Use this template" → /releases
   Sends users straight to the desktop-app release page rather than
   pivoting through /quickstart/. The flow is now concrete (download
   the binary now) instead of asking users to read an install doc as
   step 0. Once the desktop client registers a `od://skill/<slug>`
   protocol handler, this anchor flips to a JS try-deep-link-then-
   fallback without changing the page surface.

Note on the other two issues Joey raised:
- example.html 404: production has all 4 example files at HTTP 200
  (verified with curl). The 404 in his screenshot was production
  serving the previous deploy that pre-dates this PR; the fix is in
  flight, not a missing route. Once #2679 deploys, the iframe will
  resolve cleanly.
- Empty share copy: same root cause. Production HTML still rendered
  the pre-#2679 share row (no copy at all). Local dev confirms the
  X intent URL contains the full "I'm using <skill> from
  @opendesignai — the open-source Claude Design alternative…"
  string in the `text` param; Reddit submit URL contains the
  "<skill> — open-source Claude Design alternative" title; Email
  mailto: subject and body are wired. LinkedIn and Facebook are
  URL-only by their vendor design — those platforms read the OG
  meta tags from the destination page itself.

Surface area
- Marketing site only. `pages/skills/[slug].astro`,
  `pages/templates/[slug].astro`, `sub-pages.css`.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI.
- No new dependencies.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: skill detail thumb shows the live-preview overlay on
  hover; click opens the iframe in the same frame. Use this skill
  → opens https://github.com/nexu-io/open-design/releases. Same on
  the templates detail page.

* fix(landing-page): example.html copy step + share dialog with copy-then-paste flow

Two follow-ups against Joey's review of #2679.

1. example.html 404 — production was SPA-falling back to the homepage

The "404" Joey screenshotted on
`/skills/deck-guizang-editorial/example.html` was a Cloudflare Pages
SPA fallback: the URL returned HTTP 200 but the body was the
homepage HTML, so the iframe loaded "the homepage inside the iframe"
which the browser displays as broken-page. Root cause: the build
artifact never contained `out/skills/<slug>/example.html`. Astro
generates `<slug>/index.html` for the detail page from `[slug].astro`,
but the canonical `example.html` next to the SKILL.md file in the
repo root never gets copied into `out/`.

Adds `scripts/copy-example-html.ts` and chains it into the
`build` script. After `astro build`, the script walks:

  - `skills/<slug>/example.html` → `out/skills/<slug>/example.html`
  - `design-templates/<slug>/example.html` → `out/skills/<slug>/example.html`
    (design-templates surface as skill-template-origin records in the
    catalog and the iframe targets the `/skills/<slug>/example.html`
    path for those.)
  - `templates/live-artifacts/<slug>/template.html` → `out/templates/<slug>/template.html`
    (live-artifact-origin records — the iframe targets template.html.)

Source files that don't exist are silently skipped. The script
prints a summary line so the build log makes the count visible.

2. Share UX — modal with copy-then-paste flow

The previous inline 6-button row had two problems Joey called out:
  - Position was below the meta block, not prominent enough.
  - LinkedIn and Facebook ignore `text` pre-fill params, so users
    landing on those platforms saw an empty composer with no idea
    what to write. X / Reddit pre-fill works but truncates Chinese
    unpredictably.

Replaces the row with a `<dialog>` modal:
  - A `Share ↗` button sits inside `.detail-actions` next to the
    primary CTAs, so it has equal visual weight.
  - Clicking opens the dialog with the canonical share copy
    (containing the brand SEO keyword "open-source Claude Design
    alternative") in a readonly `<textarea>`.
  - `Copy text` button writes the textarea contents to the clipboard
    (with a `prompt()` fallback for older browsers) and flashes the
    coral confirmation state.
  - `Copy link only` writes just the URL.
  - Below: a row of platform jump buttons (X · LinkedIn · Reddit ·
    Facebook · Email). Each opens the vendor's compose URL in a new
    tab. The user pastes the already-copied text — uniformly
    reliable across every platform.
  - Modal closes via the × button (form method="dialog") or Escape.

Native `<dialog>` element + `showModal()` API. No new dependencies;
the JS handler lives in the existing `header-enhancer.astro`
inline script alongside the headroom + stars + hamburger handlers.

Surface area
- Marketing site only. `pages/skills/[slug].astro`,
  `pages/templates/[slug].astro`, `_components/header-enhancer.astro`,
  `sub-pages.css`, plus the new `scripts/copy-example-html.ts` and
  one-line `package.json` build script change.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI.
- No new dependencies.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: `/skills/<slug>/` shows the `Share ↗` trigger inside
  detail-actions; clicking opens the modal with the readonly
  textarea pre-filled with the canonical share copy. Copy text /
  Copy link only both flash coral on click and write to clipboard.
  Platform buttons open compose pages in new tabs.
- After deploy: `/skills/<slug>/example.html` will resolve to the
  actual canonical example output rather than SPA-falling back to
  the homepage. Same for templates.

* fix(landing-page): example.html endpoint routes + locale-aware share + brand logos

Three follow-ups against Joey's review of #2679 round 2.

1. example.html 404 — root cause + proper fix
   The 404 Joey kept seeing was real, not a deploy lag: nothing in
   the build pipeline copied `skills/<slug>/example.html` from the
   repo root into the landing-page output. Astro generated only the
   detail-page `index.html`; Cloudflare Pages SPA-fell-back to the
   homepage on requests for `example.html`, which the browser
   rendered as "wrong page in iframe" and Joey read as 404.

   Replaces the post-build copy script (`scripts/copy-example-html.ts`,
   removed) with two Astro endpoint routes:

   - `pages/skills/[slug]/example.html.ts` — streams the canonical
     example for skill-template-origin records, including the
     design-templates passthrough
     (`design-templates/<slug>/example.html` → same URL).
   - `pages/templates/[slug]/template.html.ts` — streams the canonical
     artifact for live-artifact-origin templates.

   Both use `getStaticPaths` so Astro pre-renders into the static
   build artifact under `out/`. Works in dev (Astro dev server runs
   the endpoint live) and prod (file is on disk after `astro build`).

   Required moving `pages/skills/[slug].astro` →
   `pages/skills/[slug]/index.astro` (and same for templates) because
   Astro can't have BOTH a `[slug].astro` file AND a `[slug]/`
   directory with dynamic param children at the same level. The
   `[locale]/skills/[slug].astro` re-exporters were updated to point
   at the new index files.

   `trailingSlash: 'always'` rewrites endpoint URLs to `path/`, so the
   iframe `src` and "Open in new tab" anchor now use
   `example.html/` and `template.html/` (with trailing slash). Tested
   locally: HTTP 200 + real example HTML in the body.

2. Share copy now per-locale; description dropped
   The previous template hardcoded the framing in English ("I'm using
   X from @opendesignai…") with the description following from
   `skill.description`. Joey's catch: when the SKILL.md description is
   in one language and the page locale is another, the share text
   reads as a forced bilingual mash-up.

   Adds an inline `SHARE_COPY` table per landing locale (18 entries,
   one per locale). Drops the description from the share template
   entirely — the framing + URL is enough to prompt a click, and
   removes any chance of a bilingual mismatch when SKILL.md
   frontmatter happens to be in a non-matching language.

   The brand keyword "open-source Claude Design alternative" stays
   English because that's the canonical search query Google
   associates with the domain — translating it would split the
   entity claim. Surrounding sentence translates per locale so the
   message reads as one voice.

   Same template added for templates/[slug]/index.astro.

3. Share dialog UI: brand logos for the 4 platform jump buttons; Email dropped
   Replaces the previous text labels (`X` / `LinkedIn` / `Reddit` /
   `Facebook` / `Email`) with inline-SVG brand logos. Per Joey's
   revision the Email channel was dropped — Gmail / Outlook
   pre-fill is reliable but the audience reach is much smaller than
   the four social platforms, and removing it tightens the row.

   Logos are SimpleIcons-style SVG paths inlined directly (no font
   dependency, no external icon library). Each button keeps an
   `aria-label` plus a visually-hidden `<span class="sr-only">`
   for screen readers.

Surface area
- Marketing site only. `pages/skills/[slug]/index.astro`,
  `pages/skills/[slug]/example.html.ts`,
  `pages/templates/[slug]/index.astro`,
  `pages/templates/[slug]/template.html.ts`,
  `_components/header-enhancer.astro`, `sub-pages.css`,
  `package.json` (build script revert), and the two `[locale]/...`
  re-exporters.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI surfaces.
- No new top-level dependencies.
- The two restructured detail pages keep their existing route URLs
  and existing static-paths logic — only the file location changed.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: `/skills/deck-guizang-editorial/example.html/` returns
  HTTP 200 with a 4942-byte body that's the actual canonical
  example output (not the homepage SPA fallback).
- Local dev: `/skills/deck-swiss-international/` share dialog shows
  4 brand-logo platform buttons (no Email); textarea contains the
  English-only framing + URL. `/zh/skills/...` shows the Chinese
  framing + URL with no English bleed-through.

* fix(landing-page): punchier share copy with emojis across 18 locales

The previous share template ("I'm using <name> from @opendesignai —
the open-source Claude Design alternative.\n\n<url>") was too flat to
spark a click — Joey called it out as 平淡 with the keyword
front-and-center but no hook.

New shape: three-line punchy block with emojis as visual anchors.

Skills surface (`/skills/<slug>/`):

  🎨 Just discovered <name> on @opendesignai — the open-source
     Claude Design alternative.
  ✨ Local-first · BYOK · your agent does the design.

  → <url>

Templates surface (`/templates/<slug>/`):

  🎨 Just forked <name> from @opendesignai — the open-source
     Claude Design alternative.
  ✨ Templates as files, not vendor docs. Fork → swap → ship.

  → <url>

Pattern per locale:
  - Line 1: action verb hook (`Just discovered` / `Just forked` /
    locale equivalent like `安利一个` / `推薦一個` / `Gerade entdeckt` /
    `Découvert` / etc) + skill name + brand keyword.
  - Line 2: tight value-prop with `·` separators — Local-first ·
    BYOK · agent does the design (skills) or Templates as files,
    not vendor docs (templates).
  - Line 3: → URL.

Both lines lead with an emoji (🎨 then ✨) so the post visually pops
in a feed. The brand keyword "open-source Claude Design alternative"
stays English in every locale (canonical search query for the
domain); surrounding sentence translates per locale.

All 18 landing locales rewritten — ar, de, en, es, fr, id, it, ja,
ko, nl, pl, pt-br, ru, tr, uk, vi, zh, zh-tw. Skills and templates
each have their own `SHARE_COPY` table; the templates variant has
fork-flavored framing because the user action there is fork-and-ship,
not run-once.

Surface area
- Marketing site only. `pages/skills/[slug]/index.astro` and
  `pages/templates/[slug]/index.astro`.
- No other files touched. No new dependencies.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: en / zh / ja all render with emojis intact and
  language-specific framing; X intent URL preserves the multiline
  breaks via `\n` in the `text` query param.

* fix(landing-page): restore post-build copy step for preview iframes

The detail-page interactive preview iframe pointed at endpoint routes
(`pages/skills/[slug]/example.html.ts`,
`pages/templates/[slug]/template.html.ts`) introduced in 0d9c9a5, but
Astro 6 silently drops `pages/<dir>/[slug]/<file>.<ext>.ts` routes
under dynamic segments at build time — even with `export const
prerender = true` — so the URLs returned 404 in both `pnpm dev` and
the production build.

Verified locally: dev server `curl /skills/<slug>/example.html` → 404,
`find apps/landing-page/out -name 'example.html'` → 0 files after a
clean `pnpm build`.

Restore the post-build copy step that 138cbd2 had: an `astro build`
postscript that mirrors `skills/<slug>/example.html` and
`design-templates/<slug>/example.html` into the static output. While
re-introducing the script, also address the live-artifact preview
mismatch flagged by review:

  - Live-artifact records carry a `live-` slug prefix from
    `shapeLiveArtifactTemplate()` in `_lib/catalog.ts`, so the iframe
    URL is `/templates/live-<slug>/preview.html` — copy the source
    file into `out/templates/live-<slug>/preview.html` to match.
  - Serve `index.html` (the rendered preview) rather than
    `template.html` (which still contains `{{data.*}}` placeholders).
    The iframe is for visitors and reviewers, not the template
    runtime.

Detail-page iframe `src` and "Open in new tab" link in
`pages/templates/[slug]/index.astro` already use `/preview.html`;
sub-pages.css comment kept aligned.

---------

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-05-22 20:47:59 +08:00
kamiandGitHub 586e2a0c3b Fix Draw Send to chat during streaming (#1961)
* Fix queued draw annotation send while streaming

* Wire draw send while streaming
2026-05-22 19:47:52 +08:00
YOMXXXandGitHub f08f7e390b fix(daemon): strip stale BYOK OPENAI_API_KEY for codex spawn (#2441)
Mirror the issue #398 fix the claude adapter already has: when
spawning Codex CLI without a custom OPENAI_BASE_URL, strip both
OPENAI_API_KEY and CODEX_API_KEY from the child env so Codex CLI's
own `~/.codex/auth.json` (codex login) wins.

Without this guard, a stale BYOK key left behind in
`agentCliEnv.codex.OPENAI_API_KEY` (e.g. after the user clears the
BYOK dialog and switches execution mode back to Local CLI) silently
flows through `spawnEnvForAgent` and trips 401 invalid_api_key.

The stripping is gated on OPENAI_BASE_URL so users who intentionally
route Codex CLI through a third-party OpenAI-compatible gateway keep
the credential that authenticates against it. Comparison is
case-insensitive to close the Windows mixed-case env name hole that
issue #398 already documents for ANTHROPIC_API_KEY.

Fixes #2420
2026-05-22 19:47:29 +08:00
33b96f184e fix(web): compact plugin authoring composer (#2492)
* fix(web): compact plugin authoring composer

* fix(web): compact pending plugin authoring composer

* fix(web): compact plugin authoring handoff composer

* fix(web): align plugin composer layout rebase

---------

Co-authored-by: Ghxst <200635707+GHX5T-SOL@users.noreply.github.com>
2026-05-22 19:46:47 +08:00
Siri-RayandGitHub 8eecf665da Fix Design Files workspace loading (#2703)
* Fix Design Files workspace loading

Generated-By: looper 0.8.1 (runner=worker, agent=codex)

* Fix Design Files workspace loading

Generated-By: looper 0.8.1 (runner=fixer, agent=codex)

* Fix Design Files workspace loading

Generated-By: looper 0.8.1 (runner=fixer, agent=codex)
2026-05-22 19:46:16 +08:00
22e057cc6a Reject reserved project paths in HTML artifacts (#2484)
* Reject reserved paths in HTML artifacts

* Handle reserved artifact path edge cases

---------

Co-authored-by: Ghxst <200635707+GHX5T-SOL@users.noreply.github.com>
2026-05-22 19:09:33 +08:00
YOMXXXandGitHub 48ed23c72f fix(daemon): finish live-artifact chat runs via watchdog quiet-period handoff (#1451) (#2585)
* fix(daemon): finish live-artifact chat runs via watchdog quiet-period handoff (#1451)

Live-artifact runs were staying in `Working` for the full 10-minute
inactivity window even after the deliverable had been registered, and
sometimes finishing as `failed` with `Agent stalled without emitting
any new output for 600s`. The agent process kept its stdin/stdout
alive (claude-code stream-json idle stdin, post-write reasoning that
never reaches the chat) so the existing watchdog could not tell the
deliverable was already in the user's hands.

Wire `/api/tools/live-artifacts/create` back into the chat run via a
small per-run handle registry: on the first `created` event, the run
flips a local `artifactRegistered` flag and rearms the watchdog with
the shorter `OD_CHAT_RUN_ARTIFACT_QUIET_PERIOD_MS` (default 60s)
instead of the 10-minute pre-artifact ceiling. When that quiet timer
trips, the watchdog no longer emits a stalled-error / `failed` finish;
it SIGTERMs the child and lets the existing child-exit handler do
final classification — the close handler now treats a SIGTERM exit
after a registered artifact as `succeeded`, matching what the user
actually got (a delivered artifact, not a failed run).

The handoff stays with the existing child-exit lifecycle, so tool
token revocation, cancel semantics, and exit-status classification
keep their current owner — addressing the PR #1543 review history
where finishing the run from the tool route bypassed those guarantees.

Closes #1451.

* fix(daemon): gate artifact quiet-period close on daemon-initiated flag (#1451 review follow-up)

Reviewer (#2585) found that the close-handler branch reclassifying
SIGTERM/SIGKILL as `succeeded` only checked `artifactRegistered`, so an
unrelated later termination (external `kill`, OOM, container shutdown)
after a successful artifact write would silently flip the run from
`failed` to `succeeded` — the exact "completed without producing
anything visible" failure mode the existing close handler is trying
to prevent.

Track the watchdog-initiated shutdown explicitly: set
`artifactQuietShutdownRequested = true` immediately before
`failForInactivity()` sends SIGTERM (covering the kill-grace SIGKILL
escalation under the same flag), and require that flag in the close
handler's quiet-period branch.

Extract the final-status decision into a pure
`classifyChatRunCloseStatus` so the daemon-initiated vs external
signal cases can be pinned with focused unit tests instead of
asserting closure-internal state via end-to-end timing.

* fix(daemon): treat OD_CHAT_RUN_ARTIFACT_QUIET_PERIOD_MS=0 as disabled (#1451 review follow-up)

Reviewer (#2585 non-blocking) found that an operator override of
`OD_CHAT_RUN_ARTIFACT_QUIET_PERIOD_MS=0` no longer behaved as a
"disable the quiet period" knob: once the artifact was registered,
`activeInactivityTimeoutMs()` dropped to 0, `noteAgentActivity()`
early-returned without clearing the prior timer, and the pre-artifact
10-minute timer kept running while further agent activity stopped
refreshing it.

Make the quiet-period switch conditional on a positive value. A 0
override now means "do not shorten after artifact registration" — the
pre-artifact ceiling stays active, subsequent activity continues to
reschedule it, and the existing pre-artifact stalled-error path still
fires when the agent genuinely hangs. Pin the resolver as a pure
`resolveActiveInactivityTimeoutMs` helper so the four quiet-vs-pre
matrix cases are unit-tested directly.

* fix(daemon): arm the quiet-period watchdog when pre-artifact timeout is disabled (#1451 review follow-up)

Reviewer (#2585 non-blocking, round 3) found that
`OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS=0` paired with
`OD_CHAT_RUN_ARTIFACT_QUIET_PERIOD_MS>0` left the watchdog disarmed
forever. The `noteAgentActivity()` call at run start exited early
because the pre-artifact delay was 0, so `inactivityTimer` was still
`null` when the artifact was registered, and the prior
`if (inactivityTimer) noteAgentActivity()` guard inside
`noteArtifactRegistered()` then skipped the re-arm. The
newly-positive quiet-period delay never armed a timer at all — a
chat run that went silent right after artifact creation would stay
`running` indefinitely.

Drop the guard. `noteAgentActivity()` is already the function that
decides whether to schedule (it bails when the active delay is 0),
so calling it unconditionally keeps the behavior coherent across the
four pre/quiet combinations: both non-zero (was already fine), pre=0
+ quiet>0 (now arms the quiet timer), pre>0 + quiet=0 (still falls
back to the pre-artifact ceiling via the existing resolver), both
zero (still no watchdog at all — operator opted out).

Pure-function coverage of the ceiling decision stays in
`resolveActiveInactivityTimeoutMs` — exercised across the same four
combinations in the existing unit suite.
2026-05-22 19:06:13 +08:00
Het SavaniandGitHub 53ecd36b9d fix: preview close button hover (#2671)
* fix: preview close button hover

* fix: restore preview close button visibility on hybrid devices
2026-05-22 19:05:42 +08:00
lefarcenandGitHub 31700c046c fix(analytics): fill missing DS event fields + submit_revision + studio apply (#2717)
Four follow-up gaps surfaced from comparing the v2 spec field lists
against live PostHog data on a DS-create + DS-detail walkthrough:

1. **`design_system_apply_result` had no `page_name=studio` carrier.**
   PR #2706 wired the home variant (NewProjectPanel picker) but the
   in-project header picker (`ProjectDesignSystemPicker`, lives in
   ProjectView chrome) PATCHed `project.designSystemId` without
   emitting an apply row. The funnel only saw applies from the
   home-side, not from running projects switching their DS.

   Fix: `ProjectView.handleChangeDesignSystemId` emits
   `design_system_apply_result` with `page_name=studio`,
   `action=select_design_system` or `clear_selection`,
   `design_system_selection_mode=manual`, and the DS's
   origin/status mapped from the picker's `DesignSystemSummary`.

2. **`design_system_create_result.project_id` was always missing.**
   The contract had `project_id` optional but the emit site never
   populated it, even though a successful generate path opens a
   workspace project (`project.id` is in scope right after
   `ensureDesignSystemWorkspace`). `created_as_project: true` rows
   carried no project id at all.

   Fix: `emitCreateResult` signature gains a `projectId` parameter;
   the success branch passes `project.id`, failure branches pass
   `undefined`.

3. **`design_system_review_result` only emitted on Looks good /
   Needs work clicks (`review_action: looks_good|needs_work`).**
   The spec also defines `submit_revision` and `regenerate` actions.
   When a user clicks Needs work → types feedback → sends, the send
   IS the `submit_revision` lifecycle moment, but no row fired.
   Dashboards couldn't separate "picked Needs work but never sent"
   from "picked Needs work and dispatched a revision request".

   Fix: `sendProjectChatMessage` checks for `feedbackSection` (set
   by the Needs work onClick) and emits `review_action:
   submit_revision`, `result: submitted`, with the section slug and
   the feedback length bucket. `regenerate` stays out of scope —
   the current UI has no explicit regenerate button; the Generate
   path always creates a fresh DS.

4. **`page_view{page_name=design_system_project}` carried no
   `project_id`.** Both the generation-active and the
   design_system_preview emissions had `system.id` but not the
   workspace project id, so the funnel couldn't join the DS detail
   page_view to the project the user actually edits inside.

   Fix: both `trackPageView` calls pass `workspaceProjectId`; the
   useEffect deps add `workspaceProjectId` so a delayed ensure
   (workspace mounts after first render) re-emits with the now-
   known id.

Validation:

  pnpm --filter @open-design/web typecheck   clean
  pnpm --filter @open-design/web test         1844/1844
2026-05-22 19:04:12 +08:00
Devayan DewriandGitHub 1b908a8481 fix(daemon): restore full assistant turn after mid-flight reload reattach (#2383)
* fix(daemon): restore full assistant turn after mid-flight reload reattach

When a daemon run is in progress and the browser reloads, the client
reattaches and the artifact recovers, but the restored chat turn drops
assistant text, thinking events, and producedFiles. Three independent
defects combine to cause this:

1. The reattach onDone never populated producedFiles. The pre-turn file
   snapshot used as the diff baseline lived only in a closure. Now it is
   persisted on the assistant message as preTurnFileNames so the reattach
   path can rebuild the diff after reload.

2. The SSE replay used a strict `>` cursor compare. A client that had
   already persisted lastRunEventId equal to the final event id received
   zero replay events on terminal-run reattach, fell into the status-only
   REST fallback, and never fired a clean onDone. The server now replays
   the final buffered event on terminal-run reattach when the cursor is
   at or past the end, so the client always sees a terminal signal.

3. The text buffer flushed on visibilitychange but not on pagehide.
   Hard reloads on browsers where visibilitychange does not fire before
   teardown could lose the last ~250ms of streamed text from the
   persisted message. A pagehide listener now flushes synchronously.

Refactor: extracted computeProducedFiles helper so the send and reattach
flows share the diff logic and cannot drift apart again.

Tests:
- apps/web/tests/components/ProjectView.reattach-restore.test.tsx
  covers: reattach onDone populates producedFiles from preTurnFileNames;
  reattach reaches succeeded via SSE even when only the end event replays;
  computeProducedFiles unit cases.
- apps/daemon/tests/runs.test.ts adds replay-cursor coverage for both
  the terminal-replay safety branch and the no-duplicate normal branch.

* fix(daemon): persist preTurnFileNames end-to-end on the messages table

Review on #2383 caught that `ChatMessage.preTurnFileNames` (added in
packages/contracts) had no daemon-side persistence: the messages
schema, upsertMessage, and normalizeMessage all ignored the field.
saveMessage() would PUT the field, the daemon would silently drop it,
and a real page reload would read a row without `preTurnFileNames`, so
the reattach onDone fell back to `new Set(nextFiles.map(...))` and
still missed files produced earlier in the turn.

This commit closes the round trip:

- New `pre_turn_file_names_json TEXT` column on the messages table,
  with a forward-compatible ALTER for existing databases (same pattern
  as agent_id / feedback_json / run_status).
- Both upsertMessage branches (UPDATE and INSERT) now serialize
  m.preTurnFileNames into the new column.
- listMessages, the post-upsert readback SELECT, and normalizeMessage
  surface the column back to callers.

Round-trip tests in apps/daemon/tests/db-pre-turn-file-names.test.ts
cover: write+listMessages, the UPDATE upsert path preserving the
baseline, and a legacy-row case returning undefined.

* fix(web): preserve terminal status + full multi-file diff on reattach

Two correctness issues caught in review of the prior reattach commits:

1. The reattach onDone path hard-coded `runStatus: 'succeeded'`, which
   overwrote a 'failed' or 'canceled' status that the replayed terminal
   event had already recorded via onRunStatus. Restored messages would
   come back as success even when the run had actually failed or been
   canceled. Now derives the final status from `prev.runStatus` via the
   existing `resolveSucceededRunStatus` helper, mirroring the send path
   at line 2333.

2. When `findExistingArtifactProjectFile()` recovered an existing
   on-disk artifact, the produced-files list was replaced with that
   single file, dropping any other files the turn had created earlier.
   Now always computes the full diff against `preTurnFileNames`, then
   appends the recovered artifact only if it isn't already in that
   set. Extracted as `mergeRecoveredArtifact(diff, recovered)` so the
   logic is a unit-testable invariant.

Tests in ProjectView.reattach-restore.test.tsx:
- mergeRecoveredArtifact: three cases (recovered appended to pre-files,
  no duplication when already in the diff, passthrough on no recovery).
- reattach failed-status: onRunStatus('failed') → onDone → final
  saveMessage has runStatus 'failed', not 'succeeded'.
- reattach canceled-status: same shape for cancellation.

* fix(web): force keepalive PUT on pagehide so the last buffered chunk survives reload

Review on #2383 caught that onPageHide() only called flush(), which
updates React state then schedules persistSoon() — a 500ms debounce.
On a hard reload the page tears down before that timer fires, so the
final ~250ms of streamed text never reaches the daemon.

Threaded a new flushAndPersistNow() callback through
createBufferedTextUpdates(). Both buffer call sites (send-path +
reattach-path) supply it backed by persistMessageById(id, { keepalive:
true }). saveMessage in state/projects.ts forwards the new
SaveMessageOptions.keepalive flag onto fetch's keepalive option, which
the browser honors specifically for unload-time requests.

onPageHide now calls flush() followed by flushAndPersistNow?.(), so:
- flush() pushes the buffered delta into React state synchronously
- the immediate persistMessageById then PUTs the updated message with
  keepalive:true, surviving document teardown

Regression test in ProjectView.reattach-restore.test.tsx: stream a
delta, dispatch pagehide, assert saveMessage was called with the
flushed content AND { keepalive: true } before the 500ms debounce
would otherwise have fired.
2026-05-22 18:47:12 +08:00
f799fbd7ed i18n(zh-CN): translate Manual editor and Cloudflare deploy panels (#2242)
Fill in 68 previously-untranslated strings in apps/web/src/i18n/locales/zh-CN.ts:

- Manual editor panel (manualEdit.*): 42 keys covering layer list, tabs
  (Content / Style / Attributes / HTML / Source), apply buttons,
  undo/redo, and the style editor fields (Font size, Padding, Margin,
  Radius, Border, etc.).
- File viewer viewport (fileViewer.viewport*): Desktop / Tablet /
  Mobile labels and title tooltips.
- Cloudflare deploy panel (fileViewer.cloudflare*): domain picker,
  loading / refresh / empty / failed states, subdomain prefix label
  and validation, custom-domain hint and preview, Vercel token label.
- Misc: connectors load-more / unavailable copy, designs Live / Slide /
  Media tags, orbit live-artifact kicker.

Brand names (Open Design, Cloudflare Pages, pages.dev), technical
acronyms (CLI, API, BYOK, MCP, PDF), product concepts (Orbit, Star),
and placeholder strings (notes.txt, demo, /path/to/skill-folder) are
intentionally kept in English per TRANSLATIONS.md.

Co-authored-by: zhongrenfei1-hub <231221504+zhongrenfei1-hub@users.noreply.github.com>
2026-05-22 18:45:33 +08:00
lefarcenandGitHub 85228a2b05 fix(analytics): capture About-you survey across rapid-finish flow (#2713)
PR #2590 wired About-you dropdown selections to fire one
`onboarding/ui_click` per pick (organization_size / use_case /
hear_about_us) with the chosen value attached. Live PostHog data on
nightly.11 showed a real session where every survey value was missing:
the user filled all four dropdowns and clicked Finish setup inside a
~3-second window, the route navigated away before posthog-js could
flush the per-dropdown rows, and the resulting `onboarding_complete_result`
only said `has_about_you: True` without surfacing what the user picked.
Two specific gaps surfaced.

`role` was never tracked at all. `OnboardingDropdown` for role had no
`emitOnboardingClick` call; `OnboardingClickProps` had no `role` field;
`TrackingOnboardingClickElement` had no `'role'` element. Even on a
slow-path session the dashboard could not see what role the user
identified as.

The other three fields (organization_size / use_case / discovery_source)
fired the right click events but were one round-trip away from a route
change. With per-dropdown rows as the only carrier, a Finish-setup
within a ~3-second batch lost them all to navigation-side flush failure.

Fix is two complementary delivery paths plus a closure-staleness repair:

- Contract: `TrackingOnboardingRole` (open-string, like the other
  About-you survey types so adding a future role doesn't force a
  contract bump); `role` element on `TrackingOnboardingClickElement`;
  `role?` and `use_cases?` fields on `OnboardingClickProps`. New
  `about_you_submit` click element + survey-snapshot fields
  (`role`/`organization_size`/`use_cases`/`discovery_source`) on
  `OnboardingCompleteResultProps`.

- EntryShell: emit `role/select_option` on the role dropdown so the
  per-pick funnel is symmetric with the other three. Introduce
  `profileRef` (live mirror via `useEffect`) so closures that fire
  faster than React commits (rapid multi-pick on `use_case`, the
  Finish-setup click after the last onChange) read the latest
  selection instead of stale render-time state. On Finish setup,
  emit a single `about_you_submit/continue` click with the full
  survey snapshot BEFORE `continue` + `onboarding_complete_result`,
  so the highest-value row is queued first. Mirror the same survey
  fields onto `onboarding_complete_result`, giving the dashboard two
  independent carriers for the same data — losing either path still
  leaves the funnel a complete picture.

`use_case` multi-select also had a stale-closure bug separate from
the navigation issue: the delta computation read `profile.useCase`
which was closure-captured one tick behind the latest pick. Reading
`profileRef.current.useCase` makes the delta correct even when two
picks land in the same commit. Live data from the fix session shows
`use_case` rows firing one-per-pick in real time.

Validation against the live PostHog stream on namespace
`onboarding-survey` (dev build, fresh install via wipe + restart):

  09:51:13  role/select_option            role=pm
  09:51:15  organization_size/select_option organization_size=solo
  09:51:16  use_case/select_option         use_case=product
  09:51:18  hear_about_us/select_option    discovery_source=github
  09:51:20  about_you_submit/continue      role=pm + organization_size=solo
                                           + use_cases=['product']
                                           + discovery_source=github
  09:51:20  continue/continue
  09:51:20  onboarding_complete_result     has_about_you=True
                                           role=pm + organization_size=solo
                                           + use_cases=['product']
                                           + discovery_source=github

An earlier pre-fix session on the same window (same user, no reload)
shows the original bug: 4 use_case rows, no role, no
`about_you_submit`, no survey fields on complete despite
`has_about_you: True`.

Targets release/v0.8.0.

  pnpm --filter @open-design/contracts build  green
  pnpm --filter @open-design/contracts test    111/111
  pnpm --filter @open-design/web typecheck     clean
  pnpm --filter @open-design/web test          1843/1843
2026-05-22 17:57:42 +08:00
lefarcenandGitHub 5f939ce601 fix(web): remove Ingest source panel from Automations tab (#2711)
* fix(web): remove Ingest source panel from Automations tab

The Automations tab carried a free-form "Ingest source" composer that
let users paste arbitrary content (URL, repo path, connector event,
chat snippet) and turn it into a source packet plus evolution proposals.
The form was confusing next to the routine/template flow on the same
page, exposed an internal canonicalization concept users don't need to
think about, and shipped before the surrounding evolution-proposal flow
was wired into a coherent end-to-end story.

Drop the UI surface only:

- Remove the <section className="automations-ingest"> block, the
  Template / Source / Compression / Connector selects, the title/source
  ref/content fields, the recent-packets list, and the Ingest button.
- Drop the now-dead local state (sourcePackets / sourceForm /
  ingestingSource), the patchSourceForm and submitSourceIngestion
  helpers, the SOURCE_KIND_OPTIONS / COMPRESSION_OPTIONS constants, the
  SourceIngestionForm type and DEFAULT_SOURCE_FORM, the
  /api/automation-source-packets refresh leg, and the sourcePackets
  side-write inside crystallizeRun.
- Remove the matching .automations-ingest / .automation-ingest-* CSS
  block (plus the two responsive overrides) from tasks.css.
- Delete the test case that drove the form in TasksView.templates.test.

Backend stays intact: apps/daemon/src/automation-ingestions.ts, the
POST /api/automation-ingestions route, `od automation ingest` CLI, the
routine-evolution call site, and the AutomationContentPacket /
AutomationSourceKind / AutomationTokenCompressionMode contracts all
remain, since routine scheduling still depends on them.

* fix(web): drop crystallize test assertion on removed packet list

The crystallize test was asserting that the new content packet's title
shows up on the page. That assertion only passed because the daemon
response was being side-written into the deleted sourcePackets state
and rendered in the Ingest source recent-packets strip. With that UI
removed, the packet title has no surface to land on; the proposal title
(`Skill: Artifact polish loop run`) is still asserted and remains the
real signal that crystallize succeeded.
2026-05-22 17:53:27 +08:00
10e11531a1 Improve deck home previews and plugin gallery performance (#2698)
Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-05-22 17:47:28 +08:00
Alan MatiasandGitHub 00b3f3e52d feat(design-files): add directory navigation and localization for folders (#2442)
* feat(design-files): add directory support and localization for folders in design files panel

* feat(directory-navigation): implement directory navigation and selection reset in DesignFilesPanel

* feat(rename): improve draft handling for file renaming in DesignFilesPanel
2026-05-22 17:44:01 +08:00
lefarcenandGitHub 1fd50197dd perf(landing): route HTML Anything screenshots through R2 + Image Resizing (#2696)
The `/html-anything/` landing page was loading 11 screenshots directly
from `raw.githubusercontent.com/nexu-io/html-anything/main/docs/…` —
that's 6.5 MB of raw PNGs per first visit, no AVIF/WebP transcoding,
and a separate cross-origin TLS handshake the rest of the site doesn't
pay. The TODO comment at line 70 of the page has been calling this out
since the page landed.

This PR routes those images through the same pipeline as `hero.png`:

- Source PNGs upload to `static.open-design.ai/landing/assets/html-anything/`
  on the existing `open-design-static` R2 bucket. Upload package lives
  at `~/Downloads/r2-html-anything-orig/` (README + upload.sh ready);
  merge AFTER running that script.
- Page uses `imageAsset('html-anything/<name>.png', { width, quality })`
  so CF Image Resizing emits `format=auto` AVIF/WebP variants — same
  contract every other landing image already uses.
- Banner is the LCP element; gets `srcset` across 768/1280/1920/2400
  (mirroring `heroImageSrcset`) plus its existing `fetchpriority="high"`.
- 10 below-the-fold screenshots become `<LazyImg>` so they pick up the
  precise IntersectionObserver (rootMargin 300px) instead of Chrome's
  native lazy load that over-prefetches up to 3000px.

Expected on first visit (mobile, AVIF-capable browser):
  banner:        ~150 KB AVIF      (was 2.05 MB PNG)
  10 screenshots ~80-120 KB each  (was 500-900 KB PNG each)
  total          ~1.2-1.5 MB      (was 6.5 MB)
  LCP            estimated -800ms to -1.5s
2026-05-22 17:21:00 +08:00
lefarcenandGitHub 9912fa899a feat(analytics): full design-system event family + DS run variant (#2706)
Lands the v2 PostHog spec's P0 design-system event family: five new
result events covering source ingest, create, review, status, and
picker apply; the existing file_upload_result + run_created/run_finished
schemas widened to discriminate DS workspaces from regular chat runs.

Contract (packages/contracts/src/analytics/events.ts):
- AnalyticsEventName gains design_system_{source_ingest,create,review,
  status,apply}_result.
- Props interfaces + bucket/origin/method/status enums per spec.
- TrackingProjectKind gains 'design_system' for DS-as-project runs.
- RunCreatedProps / RunFinishedProps widen page_name+area to discriminate
  chat_panel vs design_system_project; entry_from union accepts DS values;
  DS-variant context fields (ds_source_origin, source_count, brand
  description length bucket, per-source counts, design_system_created,
  preview_module_count, missing_font_count).
- FileUploadSurface union adds design_systems / design_system_source.
- Bucket helpers (designSystemLengthBucket, folderCountBucket,
  totalSizeBucket), module slug + type derivation, repo host parser.

Web emission sites:
- DesignSystemFlow.generate(): create_result + threads
  prepareCreatedDesignSystemProject with analyticsTrack so each of the
  4 source paths emits source_ingest_result (success / partial / failed
  / empty), repo-host dominance, fallback type from connector status.
- DropZone onFiles handlers: file_upload_result with deriveUploadCohort.
- DesignSystemDetailView: status_result on togglePublished + Make-default,
  review_result on Looks-good / Needs-work; module_id from markdown
  section header slug (designSystemModuleSlug), module_type via keyword
  heuristic.
- DesignSystemsTab: status_result on publish toggle, set/unset default,
  delete (incl. cancelled when window.confirm dismissed).
- NewProjectPanel: apply_result on DS picker change (manual select +
  clear) plus an auto_select emit when the picker mounts with a default
  DS not yet user-touched.
- ProjectView.streamViaDaemon: when project.metadata.importedFrom ===
  'design-system', pass analyticsHints with entry_from
  (onboarding_design_system for the auto-sent first message,
  regenerate_from_review for subsequent sends), projectKind=design_system,
  designSystemRunContext.

Daemon:
- ChatRequest gains optional analyticsHints (entryFrom / projectKind /
  designSystemRunContext). Behavior never depends on these; only PostHog
  props do.
- /api/runs handler reads analyticsHints to flip baseProps to the DS
  variant (page_name=design_system_project, area=design_system_generation,
  project_kind=design_system) when the run is DS-flagged, and spreads the
  DS context fields onto run_created.
- run_finished mirrors the DS area + adds design_system_created (true iff
  the run wrote DESIGN.md), preview_module_count (distinct preview/*.html
  writes), missing_font_count (0 placeholder; pending font-audit hook).
- run-artifacts.ts: extracts collectWrittenPathsMatching as the shared
  Write/Edit + isError-pair core; adds didRunCreateDesignSystemFile and
  countDesignSystemPreviewModules using the same dedup + failure-skip
  invariants as countNewHtmlArtifacts.

Tests:
- packages/contracts/tests/analytics-design-system-helpers.test.ts: 18
  new test cases over the bucket helpers, module slug + type mapping,
  repo host parser.
- apps/daemon/tests/run-artifacts.test.ts: 9 new tests for
  didRunCreateDesignSystemFile + countDesignSystemPreviewModules covering
  Write-then-Edit dedupe, case-insensitive DESIGN.md match, isError pair
  skip, preview/index.html as a module, non-preview path rejection.

Targets release/v0.8.0.
2026-05-22 17:18:57 +08:00
YOMXXXandGitHub 3e2f037730 feat(daemon): add CTA hierarchy static QA pass (refs #2251) (#2427)
* feat(daemon): add CTA hierarchy static QA pass

Introduce apps/daemon/src/qa/cta-hierarchy.ts exporting a pure
analyseCtaHierarchy(html) that parses generated prototypes with cheerio
and flags three precision-biased findings: multiple-primary CTAs in the
same section, ambiguous-weight (all CTAs share identical class + inline
style), and misleading-prominence (secondary-coded copy like "Learn
more" / "了解更多" styled with primary weight).

CTA candidates come from <button>, <a>, role="button" with btn/button/cta
class markers plus CTA copy keywords covering both English (Get started,
Sign up, Buy, Subscribe, Learn more, ...) and Chinese (立即购买,
立即下单, 了解更多, ...). Weight is inferred from class tokens
(primary/solid/filled/accent/cta) and from non-transparent inline
background-color, matching the inverse of the issue #2251 sample where
the header CTA was rendered with the neutral .btn style.

This PR only ships the pure function plus its tests. HTTP route, CLI
subcommand, and any auto-repair feedback loop are deliberate follow-ups
so the first cut can land without touching the daemon HTTP surface.

Refs #2251

* fix(qa): respect container boundaries in CTA hierarchy heuristics

Two precision fixes from review of #2427:

- computeContainerKey()'s parent fallback keyed by tag name alone, so
  flat layouts like <div><a class=btn-primary>...</a></div> repeated
  for sibling cards all landed in 'parent:div' and
  detectMultiplePrimary() reported a fake shared-section conflict on
  what is in fact one CTA per card. Switch to parent-node identity
  (positional index of the matched parent within its tag group, same
  trick the landmark branch already uses), so each sibling wrapper
  gets its own bucket.
- detectAmbiguousWeight() compared signatures across the entire
  document, so two unrelated sections each containing one '.btn' CTA
  with matching style would trigger 'ambiguous-weight' despite neither
  container having 2+ CTAs. The PR body's rule is narrower — 'every
  CTA in a container shares the same class + inline style' — so bucket
  by containerKey first and only emit the finding for containers with
  2+ CTAs whose signatures are identical.

Tests lock both behaviors down:
- sibling <div> card-grid without a landmark ancestor stays under the
  multiple-primary threshold;
- one-CTA-per-section pairs stay under the ambiguous-weight threshold.
2026-05-22 16:53:14 +08:00
3743c65e6e fix(web): allow configured dev origins (#2216)
Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-22 16:44:16 +08:00
YOMXXXandGitHub ca8de8bd0b fix(web): localize plugin card actions for Chinese locale (#2424)
PluginCard 中 Details/Use/Use with query/Applying…/Publish/Contribute/
Starting… 等按钮文本及 aria-label/title 此前为硬编码英文,导致中文
locale 下仍显示英文。新增 pluginCard.* 命名空间下 15 个 i18n key,
补全 zh-CN 简体与 zh-TW 繁体翻译;其他 16 个 locale 走 ...en 字典
fallback,保留既有显示行为。

Fixes #2079
2026-05-22 16:43:54 +08:00
Siri-RayandGitHub e6da01e998 Add i18n metadata for official content (#2692) 2026-05-22 16:39:32 +08:00
Patrick AandGitHub c5e38bbe58 fix(prompts): remove 10-item cap from discovery TodoWrite plan (#2298)
* fix(daemon): remove 10-item cap from discovery TodoWrite plan prompt

The RULE 3 sentence in DISCOVERY_AND_PHILOSOPHY told the model to write
'a plan of 5–10 short imperative items'. That upper bound caused the agent
to cap every plan at exactly ten steps even when the task genuinely needed
more. The TodoWrite JSON schema imposes no maxItems constraint, so the cap
was entirely prompt-driven.

Replace '5–10 short imperative items' with 'short imperative items covering
the work'. TodoWrite intent, RULE 3 label, and planning-before-building
requirement all survive unchanged.

Red spec: apps/daemon/tests/prompts/discovery-todo-cap.test.ts

* fix(prompts): remove 10-item cap from contracts discovery copy and harden tests

[pass-6,7 BLOCKER] packages/contracts/src/prompts/discovery.ts still had
the old '5-10 short imperative items' wording. apps/web imports
composeSystemPrompt from @open-design/contracts (ProjectView.tsx:43),
so web-originated chat runs were still subject to the cap.

[pass-8 WARNING] discovery-todo-cap.test.ts did not cover the contracts
copy, leaving that path unguarded. Also no guard against semantically
equivalent re-introduction via 'at most / maximum / no more than'.

Changes:
- packages/contracts/src/prompts/discovery.ts: apply same wording fix as
  apps/daemon; add inline rationale comment
- apps/daemon/src/prompts/discovery.ts: add inline rationale comment
- apps/daemon/tests/prompts/discovery-todo-cap.test.ts: add 4th assertion
  blocking 'at most|maximum|no more than N item' re-introduction
- packages/contracts/tests/system-prompt.test.ts: add 5-assertion suite
  guarding the contracts copy and composed prompt output
2026-05-22 16:23:37 +08:00
Nishant ChhillarandGitHub f183f937c0 fix: improve long project name rendering (#2258) 2026-05-22 16:19:06 +08:00
PerishFireandGitHub af66a929bc Fix release updater smoke recovery (#2687) 2026-05-22 16:18:33 +08:00
mehmet turacandGitHub 45c77fac41 fix(web): restore manual source edit tabs (#1854)
* fix(web): restore manual source edit tabs

* test: target active manual edit preview
2026-05-22 16:11:01 +08:00
6273874e30 fix codex stream reconnect handling (#2274)
Co-authored-by: Sumit Dhawan <sumitdhawan@Sumits-MacBook-Air.local>
2026-05-22 16:10:42 +08:00
Neha PrasadandGitHub 4cd93a54dd fix(web): Enter to send in project chat composer (#2676) 2026-05-22 15:57:20 +08:00
shangxinyu1andGitHub 7fd0169888 fix(web): keep trailing punctuation out of bare chat URLs (#2678)
* fix(web): keep trailing punctuation out of bare chat URLs (#2675)

* test(web): broaden bare URL punctuation regression coverage

Generated-By: looper 0.6.0 (runner=fixer, agent=codex)

* test(web): broaden bare URL punctuation regression coverage

Generated-By: looper 0.6.0 (runner=fixer, agent=codex)

* test(e2e): harden smoke suite tools-dev port startup

Generated-By: looper 0.6.0 (runner=fixer, agent=codex)
2026-05-22 15:49:05 +08:00
PerishFireandGitHub b4e94b0534 Harden packaged updater downloads and install handoff (#2677)
* Add managed download package for updater resumes

* fix(download): clear stale pid locks

* test(e2e): harden windows updater resume smoke

* feat(updater): make update downloads silent in ui

* fix(updater): keep install handoff prompt visible

* fix(ci): build platform before download in postinstall
2026-05-22 15:44:28 +08:00
lefarcenandGitHub c30f3fbfac feat(analytics): default telemetry to on so onboarding events emit pre-disclosure (#2682)
The post-onboarding disclosure modal (App.tsx:349 — `showPrivacyConsent =
... && config.onboardingCompleted === true`) only renders after the user
completes the welcome flow. Before that, `config.telemetry` is undefined
and both the daemon-side gate
(`analytics.ts: if (cfg.telemetry?.metrics !== true) return`) and the
web-side `/api/analytics/config` drop every event the onboarding view
fires.

E2E on nightly.10 (QA, 2026-05-22 06:15+ UTC) confirmed the symptom: a
real user completed the full Connect → About you → Design system → Generate
flow but PostHog received zero `page_view pn=onboarding` / `ui_click` /
`onboarding_runtime_scan_result` / `onboarding_complete_result` rows from
their distinct_id. Other events (post-onboarding home, settings, project
creation) flowed normally because by then the disclosure had been
accepted and `telemetry.metrics` was true.

Product decision (2026-05-22): default telemetry ON. The disclosure
modal stays disclosure-style ("I get it") and Settings → Privacy
remains the one-click opt-out — same UX, only the pre-decision default
changes from off to on.

Changes:
- `apps/web/src/state/config.ts`: `DEFAULT_CONFIG.telemetry =
  { metrics: true, content: true, artifactManifest: false }` so fresh
  `loadConfig()` calls emit during the first onboarding render.
- `apps/web/src/types.ts`: comment now documents the default-on
  semantics + the opt-out path.
- `apps/daemon/src/app-config.ts`: new `applyTelemetryDefaults` helper
  fills in the same defaults when `readAppConfig` finds no telemetry
  field on disk. Helper runs on BOTH the
  installation-file-shadowing path and the fallback path. An
  explicit user opt-out (`metrics: false`) is preserved untouched —
  defaults only fill `undefined`, never overwrite a saved value.
- `apps/daemon/tests/app-config.test.ts`: 49 → 51 tests. Updated 9
  existing assertions that expected `cfg.telemetry === undefined` /
  `cfg === {}` to expect the new default; added 2 regression guards:
  - "preserves an explicit telemetry opt-out across reads" pins the
    `metrics: false` invariant so a future refactor can't silently
    re-enable opted-out users.
  - "preserves a partial explicit telemetry (metrics on, content off)"
    pins per-field user choices against the default fill.

Validation:
- `pnpm --filter @open-design/daemon exec vitest run tests/app-config.test.ts` ✅ 51/51
- `pnpm --filter @open-design/web typecheck` ✅
- `pnpm --filter @open-design/daemon typecheck` ✅
- `pnpm --filter @open-design/web test` ✅ 201 files / 1839 tests
2026-05-22 15:42:20 +08:00
Golden Obsidian LLCandGitHub aa0616062d refactor(daemon): introduce HTTP Request Adapter + typed Deps (#2636)
* refactor(daemon): introduce HTTP Request Adapter + typed Deps (proof on active-context-routes)

Adds a typed HTTP boundary Adapter under apps/daemon/src/http/ that replaces
the untyped ServerContext service-locator pattern (30+ fields, mostly any)
for route handlers. Routes become pure (input, deps) -> Result<output>
functions, unit-testable without Express or supertest.

Six new modules under apps/daemon/src/http/:
  - types.ts        Result<T,E>, ok(), err(), JsonRouteSpec, Handler,
                    RouteInputContext, HttpMethod, InputParser
  - parse.ts        rawInput(req), validationError(message, issues?)
  - response.ts     sendJson(), sendApiError(), statusForError() +
                    ERROR_STATUS_BY_CODE map
  - origin-guard.ts guardSameOrigin(req, origin) wrapping isLocalSameOrigin
                    as a Result
  - adapter.ts      defineJsonRoute(), mountJsonRoute() (only place that
                    knows about req/res)
  - index.ts        barrel

active-context-routes.ts migrated as proof of pattern. parsePostActive(),
handlePostActive(), handleGetActive() are now pure functions; postActiveRoute
and getActiveRoute are exported route specs. The wire signature
registerActiveContextRoutes(app, ctx) is preserved so server.ts is untouched.

Spec at specs/current/daemon-http-adapter.md captures the strangler migration
order for the remaining route files (mcp-routes, chat-routes, artifact
routes, etc.) and a StreamRoute follow-up where the Run Orchestrator lands.

Wire-format note: cross-origin response moves from the legacy
{ error: 'cross-origin request rejected' } shape to the structured
{ error: { code: 'FORBIDDEN', message: ... } } shape. Backwards-compatible
via the existing CompatibleErrorResponse = ApiErrorResponse | LegacyErrorResponse
union in @open-design/contracts.

Validation:
  - pnpm install (post-rebase, exit 0)
  - pnpm --filter @open-design/daemon typecheck (both tsconfig.json and
    tsconfig.tests.json silent => pass)
  - pnpm --filter @open-design/daemon test: 15 new tests pass
    (tests/http/adapter.test.ts + tests/active-context-routes.test.ts).
    84 pre-existing failures across 23 files are unchanged and unrelated
    to this PR (Windows symlink / short-name / colon-in-filename, upstream
    behavior drift, missing plugin marketplace fixtures, and a freshly-
    added tools-connectors-cli suite of 38 failures that landed during
    the rebase).

Sharpens W4/W5 of specs/current/maintainability-roadmap.md and unlocks
W6 (Run Orchestrator).

* chore: add core-js, electron-winstaller, protobufjs, sharp to pnpm.onlyBuiltDependencies
2026-05-22 15:20:15 +08:00
558fedd207 fix(landing): wire GA4 rollout config (#2615)
Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 14:56:58 +08:00
72c8e34bc9 Polish home onboarding and community presets (#2658)
Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-05-22 14:26:56 +08:00
lefarcenandGitHub 5f7d65d513 perf(landing): preconnect api.github.com + rAF-throttle scroll listener (#2666)
Two PSI-targeted wins (split from #2599 follow-up).

1. New `resource-hints.astro` mounted in every page's <head> declares
   `<link rel="preconnect" href="https://api.github.com" crossorigin>`.
   The inline enhancer script on /` issues 3 fetch() calls to
   api.github.com right after DOMContentLoaded (stars, latest release,
   contributors). Without preconnect each pays a full DNS + TCP + TLS
   handshake (~150-300ms) inline with the fetch. With preconnect those
   handshakes happen in parallel with HTML parse and all three share one
   warmed HTTP/2 connection.

2. Wrap the scroll listener's read + classList write in
   requestAnimationFrame. Trackpads and high-rate wheels fire scroll
   faster than display refresh, and every callback that hits classList
   triggers layout recalc. PSI was attributing ~700ms of "forced reflow"
   to the un-throttled version. The rAF gate collapses each burst to one
   DOM mutation per frame; `{ passive: true }` is preserved so the
   listener still doesn't block the scroll thread.

   Same throttling pattern mirrored to `header-enhancer.astro` (used by
   every sub-page) and `home-enhancer.astro` (kept in lockstep even
   though /` currently uses its own inline copy).

Expected PSI delta:
  - "Preconnect to required origins" hint: cleared
  - "Forced reflow" diagnostic 700ms → near zero
  - LCP: small bonus from earlier GH fetch warm-up (~100-300ms)
2026-05-22 14:06:39 +08:00
shangxinyu1andGitHub cc6edb9afe Proxy GitHub metadata through the daemon (#2654)
* Proxy GitHub metadata through the daemon

* fix(contracts): share GitHub metadata responses

Generated-By: looper 0.6.0 (runner=fixer, agent=codex)

* fix(contracts): align GitHub fetchedAt payload types

Generated-By: looper 0.6.0 (runner=fixer, agent=codex)

* Proxy GitHub metadata through the daemon

Generated-By: looper 0.6.0 (runner=fixer, agent=codex)
2026-05-22 14:06:07 +08:00
Yuhao ChenandGitHub 83c4ef979a Polish onboarding dropdown menu (#2601) 2026-05-22 14:05:41 +08:00
Yuhao ChenandGitHub bddec2391d Fix Simple Deck discovery form contract (#2602) 2026-05-22 14:05:24 +08:00
정수현andGitHub 63c8dd1fed fix(web): localize the Routines settings page (#2598)
The Settings → Automations/Routines section (RoutinesSection.tsx) used
hardcoded English for every label, action, status pill, schedule
description, time suffix, and dialog. With the app language set to
Chinese the whole section stayed in English, breaking the localized
experience.

Introduce a dedicated `routines.*` i18n namespace, wire RoutinesSection
and the SettingsDialog routines header through `useT()`, and add
Simplified + Traditional Chinese translations. Schedule summaries are
now built from interpolated keys instead of concatenated English
literals, and the 12-hour AM/PM suffix is localized too. The remaining
locales fall back to English via the existing `...en` spread, matching
the repo's incremental per-locale translation pattern.

A regression guard in locales.test.ts asserts the representative
`routines.*` keys are actually translated (not English fallback) in
zh-CN and zh-TW.
2026-05-22 14:05:11 +08:00
Het SavaniandGitHub bfdd029d7d feat(web): polish automation schedule picker and summary pill UI (#2593)
* feat(web): polish automation schedule picker and summary pill UI

* refactor(web): share automation schedule summary formatting

* fix(web): scope automation pill flex layout to schedule segments
2026-05-22 14:04:51 +08:00
nmsnandGitHub 575d5aaf2f fix(web): strip trailing punctuation from markdown link URLs (#2568) (#2591) 2026-05-22 14:04:39 +08:00
Fl0rencessandGitHub 5b53c44e13 fix: enable SenseAudio BYOK TTS (#2570)
* fix: enable SenseAudio BYOK TTS

* fix: handle BYOK SenseAudio TTS failures

* fix: harden SenseAudio BYOK TTS responses

* fix: preserve BYOK speech tool failure kind

* fix: handle versioned SenseAudio TTS base URLs
2026-05-22 14:04:29 +08:00
lefarcenandGitHub 7f03030f3f perf(landing): self-host fonts + inline critical CSS (#2599)
* perf(landing): self-host fonts + inline critical CSS

PageSpeed Insights flagged ~2.3s of render-blocking on /:
  globals.css   12.9 KB external link, 160ms
  fonts CSS     2.2 KB  fonts.googleapis.com, 750ms
  + 4 woff2     ~1200ms each from fonts.gstatic.com

Two changes drop that whole chain:

1. Self-host fonts via @fontsource-variable/{inter,inter-tight,
   playfair-display,jetbrains-mono}. Each family ships a single variable
   woff2 (covers all weights we use) that Astro bundles into /_astro/*
   alongside the rest of the build, served same-origin through CF Pages —
   no separate TLS handshake, no Google Fonts CSS round-trip. The CSS
   variable names get an extra alias in front (`'Inter Tight Variable',
   'Inter Tight', ...`) so a system fallback still works if the package
   ever ships under a different family name.

2. `astro.config.ts: build.inlineStylesheets: 'always'` inlines every
   emitted <style> into the HTML <head> instead of emitting a separate
   /_astro/*.css link. The HTML grows from ~13KB to ~28KB (gzip) but
   loses one stylesheet round-trip + the entire @font-face chain that
   used to gate text rendering.

Component cleanup: the `<FontStylesheet>` component (preconnect + link to
fonts.googleapis.com) is no longer needed and is deleted, removed from
all 7 places that mounted it. og.astro keeps its own font setup since
it renders to a screenshot.

Expected effect (from PageSpeed Insights "Render-blocking requests"
diagnostic on the previous build):
  FCP  1.9s → ~1.2s
  LCP  2.2s → ~1.5s

Verified: pnpm typecheck 0 errors, pnpm build 1853 pages 78s, preview
serves /_astro/*.woff2 as font/woff2 same-origin, 0 fonts.googleapis or
fonts.gstatic references in the built HTML.

* perf(landing): include Playfair italic + bump nix pnpm-deps hash

Two follow-ups on the self-host fonts PR:

1. globals.css imported only `@fontsource-variable/playfair-display`,
   which ships @font-face for font-style: normal only. The previous
   Google Fonts URL included the italic axis (`ital,wght@0,500;1,400;
   ...`) and several rules (.roman, .work-rule .roman, .sec-rule .roman,
   plus 8 other italics across globals.css + sub-pages.css) render
   Playfair italics via `font-family: var(--serif); font-style: italic`.
   Without the italic face self-hosted, those would fall through to
   Times New Roman italic or browser synthesis. Adding
   `wght-italic.css` keeps the typography visually equivalent.

2. nix/pnpm-deps.nix uses a fixed-output derivation hash that has to
   match the pnpm vendored store; adding the four fontsource packages
   changed pnpm-lock.yaml so the hash has to be bumped to the value Nix
   reported in CI.

Codex (Looper reviewer) flagged #1 as non-blocking.

* perf(landing): pin fontsource versions exactly per repo guard

`pnpm add` defaulted to caret ranges (`^5.2.8`) but repo guard rejects
non-exact specs ("dependency specs must be exact versions like 1.2.3 or
workspace:*"). That was the actual cause of the Preflight + Validate
workspace failures — pinning to the locked versions Codex reviewer
called out:

  @fontsource-variable/inter             5.2.8
  @fontsource-variable/inter-tight       5.2.7
  @fontsource-variable/jetbrains-mono    5.2.8
  @fontsource-variable/playfair-display  5.2.8

`pnpm guard` now passes locally (6/6 tests).
2026-05-22 11:49:16 +08:00
shangxinyu1andGitHub 95bbdbb734 [codex] test(e2e): harden settings and entry regressions (#2578)
* test(e2e): harden settings and entry regressions

* test(e2e): align entry chrome coverage with current UI

Generated-By: looper 0.6.0 (runner=fixer, agent=codex)

* fix(web): refresh saved media providers from daemon

Generated-By: looper 0.6.0 (runner=fixer, agent=codex)

* test(web): align media provider reload expectations

Generated-By: looper 0.6.0 (runner=fixer, agent=codex)

* fix(web): keep daemon media-provider reloads authoritative

Generated-By: looper 0.6.0 (runner=fixer, agent=codex)

* fix(web): make media-provider reload precedence depend on dialog edits

Generated-By: looper 0.6.0 (runner=fixer, agent=codex)

* fix(web): preserve pending media-provider edits across stale autosaves

Generated-By: looper 0.6.0 (runner=fixer, agent=codex)
2026-05-22 10:04:12 +08:00