mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
87 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
71044bd3d6
|
test(e2e): harden extended coverage state assertions (#2245)
* test(e2e): harden extended coverage contracts * docs(testing): add e2e hardening status * fix(web): persist artifact chips after daemon runs * ci: install playwright browsers for e2e vitest * Fix daemon run recovery across reloads Pin daemon-created runs to assistant messages immediately so hard reloads before the create response can reattach. Replay terminal and active run events from the beginning on reload so restored turns keep assistant text, thinking events, produced files, and artifacts. Fixes #2366 Fixes #2368 Fixes #2371 * test(e2e): preserve fake runtime selection across reload * fix(web): scope daemon run recovery to daemon mode * fix(e2e): remove duplicate delayed smoke flag * fix(web): scope replay artifact recovery to current run * fix(daemon): remove duplicate run-create pin |
||
|
|
65e760b88a
|
feat(seo): add GSC report opportunities (#2388)
Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
f294ab4915
|
chore(ci): add visual regression PR workflow (#2372)
* Add visual regression PR workflow * Allow manual visual PR comments * Post visual comments for same-repo PRs * fix(ci): surface R2 lookup failures in visual report Generated-By: looper 0.8.1 (runner=fixer, agent=opencode) * Align visual workflow names |
||
|
|
c85da3eb40
|
fix: sync landing source-of-truth paths (#2066) | ||
|
|
2c128e0e91
|
refactor desktop host bridge (#2246) | ||
|
|
07659b7272
|
feat(seo): add Search Console reporting workflows (#2229)
* feat(blog): daily 3-day Search Console traffic digest Adds `blog-3day-report.yml` (cron 09:00 Asia/Shanghai) and a companion `report-3day.ts` script that refreshes `docs/blog-traffic-digest.md` once per day. The digest has two sections: - T-3 spotlight: posts published exactly three days ago, with their 3-day Search Analytics window plus current URL Inspection coverage state. - Rolling 30-day cohort: every post 1–30 days old with its latest 3-day Search Analytics window, sorted by impressions descending. The workflow is read-only against Google APIs (no Indexing API, no "request indexing" automation) and mirrors the secret / config plumbing already used by `blog-indexing-monitor.yml`. Output lands in a reviewable `automation/blog-traffic-digest` PR opened by the open-design bot. Also widens `querySearchAnalytics` to accept `windowDays: 3 | 7 | 28` and updates `docs/blog-indexing-automation.md` with the new pipeline. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(seo): post daily Search Console report to Feishu Co-authored-by: Cursor <cursoragent@cursor.com> * feat(blog): push traffic digest to Feishu Emit a compact JSON summary from the daily 3-day traffic digest and add a Feishu custom bot sender for the summary card. Wire the workflow to send the card when `FEISHU_BLOG_DIGEST_WEBHOOK` is configured while keeping Markdown PR output as the source of truth. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(landing-page): add Discord routing CTAs Add a lightweight Discord pill to the landing hero and Discord links in the landing and blog footers so community routing is visible without displacing the primary GitHub and download CTAs. Add a blog-ending conversion card that points guide and use-case readers to the internal workflows library, while keeping Discord as a secondary support path. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
e702a6a49f
|
Fix blog indexing status PR base (#2106)
Set an explicit base branch for generated indexing status PRs so create-pull-request works after SHA-based checkouts. Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
bb13eee765
|
chore: optimize CI and beta release runtime (#2231)
* chore(ci): add runtime trace summaries * chore(ci): tighten measured workspace steps * chore(release): tighten beta setup steps * chore(release): slim beta windows smoke * chore(ci): shard daemon tests * chore(ci): harden runtime trace lookup * chore(release): avoid mac pnpm cache in beta * chore(ci): split critical playwright checks * chore(release): publish beta platforms from builders * test(e2e): update beta release workflow expectation * chore(ci): stop gating PRs on nix check * fix(release): keep beta latest complete |
||
|
|
a1e8ce480a
|
fix(ci): include bundled resources in Windows cache key (#2034) | ||
|
|
4f116d9eaf
|
fix(ci): anchor PR inactivity clock to author responses (#2185)
* fix(ci): anchor PR inactivity clock to author responses * fix(ci): add dry-run mode to PR inactivity workflow * fix(ci): read workflow dry-run input from event payload * fix(ci): log PR inactivity dry-run diagnostics * fix(ci): accept both review association field names * fix(ci): log PR 642 feedback payload shapes * fix(ci): trust PR reviewers by repo permission * fix(ci): remove temporary inactivity debug logs |
||
|
|
bd48c597b0
|
chore: pin dependency versions and harden CI caches (#2189)
* chore: pin dependency versions * ci: enforce pinned dependency specs * ci: fix pnpm executable invocation |
||
|
|
99b42726b8
|
Simplify CI PR gate (#2183) | ||
|
|
f650a043d9
|
test(e2e): align entry coverage with redesigned flows (#2101)
* Migrate entry E2E coverage and split CI * test(e2e): relax connectors auth error assertions * ci: route scenario registry changes to extended e2e * ci: decouple packaged smoke jobs from validate gate * ci: restore pre-split workflow * test(e2e): slim critical ui smoke coverage * test(e2e): move broader entry flows out of critical * test(e2e): restore entry chrome coverage to ci * ci: parallelize workspace validation jobs * test(web): stabilize media palette bridge assertion * ci: cache e2e playwright browsers |
||
|
|
f403ffbfce
|
ci: add PR-author and stale-issue inactivity workflows (#2055)
* ci: add PR-author and stale-issue inactivity workflows Adds two queue-management automations: - pr-author-inactivity: reminds PR authors after 72h of inactivity following human reviewer/maintainer feedback (issue comments, non-approval reviews, or inline review comments) and closes after 120h. Author response is detected via issue comments, inline review replies, or commit/force-push events. Bot-authored reviews are intentionally excluded so authors are not pressured by automated nits alone. - stale-issues: marks issues stale after 30 days of inactivity and closes after a further 7 days. Exempts good first issue, help wanted, and security labels. A pre-step also auto-applies 'exempt-from-stale' to issues opened by org members/owners/ collaborators, since actions/stale only supports label-based exemptions. PR processing is disabled (handled by the workflow above). * fix: limit PR inactivity feedback to trusted reviewers Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * fix: count author PR reviews as inactivity responses Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) |
||
|
|
0163fa9d84
|
fix(landing): stabilize blog indexing workflows (#2042)
Use full workspace installs for blog indexing workflows so root postinstall can resolve workspace build dependencies, and enrich sitemap metadata for blog URLs from frontmatter dates. Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
7766582f0b
|
chore(ci): scope nix-check workflow permissions to contents:read (#1870)
Some checks failed
ci / Packaged mac smoke (push) Blocked by required conditions
ci / Packaged windows smoke (push) Blocked by required conditions
ci / Detect PR change scopes (push) Failing after 2s
ci / Validate workspace (push) Has been skipped
nix-check / build (push) Failing after 1s
ci / Packaged linux headless smoke (push) Has been skipped
The other workflows under .github/workflows declare explicit
`permissions:` blocks that scope their GITHUB_TOKEN to the minimum
required (contents: read for build-only flows). `nix-check.yml` was
the lone outlier and inherited the repository's default token
permissions instead.
Add `permissions: { contents: read }` to align with the rest of the
workflow suite and follow GitHub's least-privilege workflow guidance.
No behavior change: the job only reads the repo, runs `nix flake
check`, and uploads a logs artifact on failure (which uses an action
that already declares its own permissions internally).
Co-authored-by: nicejames <nicejames@gmail.com>
|
||
|
|
6bf865a43b
|
fix(ci): avoid duplicate nix-check runs on PR branches (#1917)
Some checks failed
ci / Packaged mac smoke (push) Blocked by required conditions
ci / Packaged windows smoke (push) Blocked by required conditions
ci / Detect PR change scopes (push) Failing after 2s
ci / Validate workspace (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 1s
landing-page-deploy / Deploy landing page (push) Has been skipped
github-metrics / Generate repository metrics SVG (push) Has been skipped
nix-check / build (push) Failing after 2s
ci / Packaged linux headless smoke (push) Has been skipped
* fix(ci): avoid duplicate nix-check runs on PR branches * fix(ci): keep nix-check on main pushes Restore the nix-check push trigger for main-only updates while still avoiding duplicate PR-branch runs. Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) |
||
|
|
1f728ac8e3
|
fix(ci): only run docker image workflow for release tags (#1916) | ||
|
|
d6515643ef
|
fix(ci): use open-design-bot for metrics PRs (#1910)
* fix(ci): use open-design-bot for metrics PRs * fix(ci): restore metrics workflow app scopes Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) |
||
|
|
fd82384b5e
|
fix(ci): dispatch metrics branch validation (#1878)
* fix(ci): dispatch metrics branch validation * fix(ci): dispatch metrics branch validation Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * fix(ci): dispatch metrics branch validation Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * fix(ci): dispatch metrics branch validation Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * fix(ci): dispatch metrics branch validation Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * fix(ci): dispatch metrics branch validation Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * fix(ci): dispatch metrics branch validation Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) |
||
|
|
7d1adf9fd7
|
docs: point 0.8.0 preview contributors at main (#1846)
* docs: point 0.8.0 preview contributors at main, not preview/v0.8.0 0.8.0 has been merged into main (#1832). Anywhere we used to tell contributors to checkout / PR against preview/v0.8.0 was actively mis-routing new PRs. Update: - docs/preview-v0.8.0-announcement.md + zh-CN: status line, Branch row, source-build checkout, and 'open a PR against' guidance now point at main - .github/ISSUE_TEMPLATE/bug-report.yml + feature-request.yml: phrase the 'use the preview template' nudge as 'about the 0.8.0 preview features (now on main)' instead of 'about the preview/v0.8.0 branch' - .github/ISSUE_TEMPLATE/config.yml: same rewording for the contact link - .github/ISSUE_TEMPLATE/preview-v0.8.0-feedback.yml: refresh the description and the intro body so it reads as 'preview features pre-tag', not 'features pre-merge' The preview-v0.8.0-feedback template and preview/v0.8.0 label are intentionally kept: 0.8.0 isn't tagged yet, so we still want a dedicated lane for preview-features feedback. * chore: stop treating preview/v0.8.0 as a live branch Earlier in this PR we kept the preview-v0.8.0 surface area intact — that was the wrong call. 0.8.0 is now on main; pretending there's a parallel 'preview' branch in the templates, labels, and copy was going to keep mis-routing contributors. Drop: - .github/ISSUE_TEMPLATE/preview-v0.8.0-feedback.yml (the dedicated template that auto-applied the preview/v0.8.0 label and prefix) - .github/ISSUE_TEMPLATE/config.yml contact_links entry pointing at it - bug-report.yml + feature-request.yml nudges that sent users there - The Preview-v0.8.0-feedback link block from both announcement docs (replaced with normal bug-report / feature-request links) Rename: - docs/preview-v0.8.0-announcement.{md,zh-CN.md} -> docs/v0.8.0-announcement.{md,zh-CN.md} so the on-disk doc title reads as a 0.8.0 announcement, not a branch-specific one. No other repo file referenced the old paths. The preview/v0.8.0 label and branch themselves are intentionally untouched — those are separate ops the maintainer will decide on later. This PR only removes mentions inside the repo. * chore: keep 0.8.0 preview-feedback template as a chooser-level ad The previous commit deleted preview-v0.8.0-feedback.yml entirely. Bring it back, but reframe it: it's now the dedicated 0.8.0 lane in the issue chooser — a high-visibility surface that tells visitors "0.8.0 is here as a preview, please share what you noticed." - Renamed in the chooser to "Open Design 0.8.0 — preview feedback" - Title prefix shortened from "[preview/v0.8.0] " to "[0.8.0] " so the branch slug no longer leaks into issue titles - label preview/v0.8.0 still auto-applied (the label entity is still in use across 26 issues; maintainer will decide on its fate separately) - Area dropdown widened from "Skills + Automations" to cover the actual 0.8.0 surface (plugins, headless, agent flow, desktop shell) - Intro body rewritten to read as a preview-release ad, not a feature-branch tester request Announcement docs (English + Chinese) also routed their "open an issue" CTA back through this template instead of the generic bug-report / feature-request links — same advertising goal. |
||
|
|
e40399d39a
|
Merge pull request #1832 from nexu-io/sync/main-into-preview-v0.8.0
Release preview/v0.8.0 into main |
||
|
|
64139db375 |
ci(diag): capture packaged-linux runtime logs into the headless artifact
Currently the artifact only contains vitest.log + the tools-pack build log. The packaged daemon's own stdout/stderr lands in $RUNNER_TEMP/tools-pack/runtime/linux/namespaces/<ns>/logs/desktop/latest.log, outside the artifact path, so when 'headless-root.json not written within 35s' fires we have no signal on what the spawned child actually did. Copy the runtime logs/ and runtime/ subdirectories into the report dir before upload so the artifact captures daemon/web sidecar startup output, identity markers, and per-app state JSON. |
||
|
|
41bae9b7a5 |
ci: mark packaged-linux-headless smoke as non-blocking until Linux ships
The v0.8.0 launch doc explicitly lists Linux as "coming soon" — the packaged Linux headless runtime is known-incomplete in this release. Keep the smoke job running so we keep collecting signal, but don't block PRs on it until the Linux client lands. Re-enable (drop continue-on-error) once the Linux packaged client is ready in a future release. |
||
|
|
772ef97476
|
feat(landing): automate blog indexing monitoring (#1825)
* feat(landing): add blog indexing automation Automate supported blog discovery checks through sitemap submission, URL Inspection monitoring, IndexNow notifications, and guarded SEO CI checks. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(landing): support oauth for blog indexing Use OAuth refresh-token auth as the preferred Search Console path while keeping service-account auth as a fallback, so the indexing workflows can run despite GSC service-account invite issues. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(landing): tighten blog indexing observability Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
22a3b99a47 |
Merge origin/main into preview/v0.8.0
Sync 49 commits from main. Conflicts resolved:
- .github/workflows/ci.yml: kept v0.8.0 granular per-area gating, added main's
linux specs + release-stable.yml + release-preview.yml triggers
- .github/workflows/release-preview.yml: kept v0.8.0's full workflow over main's placeholder
- apps/web/src/components/AssistantMessage.tsx: combined v0.8.0 file-ops
summary with main's stripTodoToolGroups + suppressAskUserQuestionFallbackText
- apps/web/src/components/ChatPane.tsx: kept both new imports
- apps/web/src/index.css: kept both .msg-plugin-chip and .user-copy-btn blocks
- e2e/ui/*.test.ts: kept v0.8.0 openEntrySettingsDialog helper over main's
inline dialog navigation (UI was redesigned in v0.8.0)
- nix/package-{daemon,web}.nix: kept v0.8.0 pnpmDepsHash; rerun nix build to refresh
|
||
|
|
30821f3a73
|
fix(ci): let metrics PRs trigger required checks (#1801) | ||
|
|
74637f1cb5
|
Add Linux packaged client parity smoke coverage (#1204)
* docs: plan linux client issue 709 * fix: complete linux headless lifecycle routing * feat: add linux packaged inspect * test: add linux headless packaged smoke * ci: add linux headless packaged smoke * ci: smoke linux AppImage release artifacts * docs: document linux packaged client status * chore: finalize linux client audit remediation * docs: add linux client publication packet * test: harden linux client smoke coverage * ci: preserve linux smoke audit evidence * refactor: consolidate linux e2e helpers Move pathExists and the desktop/web/daemon app-key array out of linux.spec.ts into linux-helpers.ts, where expectPathInside and linuxUserHome already live. Keeps the spec file focused on tests and the helpers file as the canonical home for shared Linux e2e utilities. * fix: move linux e2e helpers to lib * fix: address linux release review blockers * fix: drop npm dependency from containerized linux build writeAssembledApp() previously called runNpmInstall() which executed `npm install` directly. Inside the containerized build path, electronuserland/builder:base strips npm/npx/corepack, so the inner tools-pack build would fail at the assembled-app install step. Route the install through OD_TOOLS_PACK_PNPM_BIN: buildDockerArgs sets the env to the standalone pnpm binary it bootstraps, and the new resolveProductionInstallCommand helper consumes that env to run `<bin> install --prod --no-lockfile --config.node-linker=hoisted`. Host invocations with no env set keep the prior npm behavior. --config.node-linker=hoisted preserves the flat node_modules layout that electron-builder packs the same way as npm-installed trees. New tests cover the resolver branches and assert the docker-arg-to- resolver chain end-to-end so reviewers can see the container's inner build receives the env that switches its install away from npm. * fix: harden linux container bootstrap * fix: validate desktop marker liveness in headless cleanup cleanup --headless previously skipped on any parseable desktop-root.json, trapping recovery when the AppImage had crashed and left a stale marker. Validate the marker the same way stopPackedLinuxApp does: if the PID is not in the live snapshot list, proceed through cleanup instead of skipping. Extract the validation into validateDesktopAppImageMarker so the stop and cleanup paths share one definition of live and owned. Tests cover both branches: a stale marker drives cleanup to remove the runtime/output roots, while a live marker drives cleanup to skip and preserve them. |
||
|
|
75498838a9
|
chore: align issue templates to preview/v0.8.0 naming (#1723)
Some checks failed
ci / Packaged mac smoke (push) Blocked by required conditions
ci / Packaged windows smoke (push) Blocked by required conditions
ci / Detect PR change scopes (push) Failing after 3s
ci / Validate workspace (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 1s
landing-page-deploy / Deploy landing page (push) Has been skipped
nix-check / build (push) Failing after 1s
Following the rename of the feature branch from preview/0.8.0 to preview/v0.8.0 (to match the release/v0.7.0 convention), update all issue-template references so the label, filename, and deep-link URL stay consistent. Changes: - git mv preview-0.8.0-feedback.yml → preview-v0.8.0-feedback.yml - update labels reference, title prefix, display name, body copy - update version placeholder example to 0.8.0-preview.2 (current build) - update cross-references in bug-report.yml and feature-request.yml - update config.yml first contact_link URL + about text |
||
|
|
4f15c33595 | Merge remote-tracking branch 'origin/preview/0.8.0' into preview/v0.8.0 | ||
|
|
9218fd649e
|
feat(ui): add copy to clipboard functionality for user messages with … (#1669)
* feat(ui): add copy to clipboard functionality for user messages with localization support * fix(web): use setTimeout instead of window.setTimeout for correct Timeout type * docs: add copy prompt button screenshot for PR #1669 * docs: add copy button hover screenshot for PR #1669 * docs: add copy button copied state screenshot for PR #1669 * fix(ui): reset button border/background on copy prompt button The .user-copy-btn inherited border and background from the base button CSS, rendering as a bordered gray box instead of a clean icon overlay. This was especially visible in the Electron desktop app. Add border: none and background: none to the button, and a subtle hover background for feedback. |
||
|
|
4693ddb00d
|
chore: add issue templates (bug, feature, preview/0.8.0) + chooser config (#1708)
* chore: add issue template for preview/0.8.0 feedback Adds a guided issue form so community testers of the preview/0.8.0 branch (Skills tab + Automations) can submit structured feedback. The template auto-applies the preview/0.8.0 label, which lets maintainers filter all preview-related reports in one view: https://github.com/nexu-io/open-design/issues?q=is%3Aopen+label%3A%22preview%2F0.8.0%22 * chore: add generic bug-report issue template Pairs with the preview/0.8.0 template added in the previous commit. Until now the repo had no issue templates at all, which meant New Issue opened a blank textarea by default. The bug-report template: - Pre-applies the 'bug' label - Guides users through repro steps, version, platform, logs - Includes a callout pointing preview/0.8.0 testers to the dedicated feedback template so the two flows stay separate * chore: add feature-request template + chooser config Rounds out the issue-template basics: - feature-request.yml — 'what problem are you trying to solve' framing, willing-to-contribute dropdown so maintainers can route PRs - config.yml — disables blank-issue entry, redirects Q&A / Ideas / Show-and-tell / general chat to Discussions, points preview/0.8.0 reporters at the dedicated template After merge, the chooser at /issues/new/choose will be: Template 1. 🐛 Bug report 2. 💡 Feature request 3. 🧪 Preview 0.8.0 feedback Contact → Preview 0.8.0 feedback (dup, easy-access) → Ask a question (Discussions Q&A) → Discuss an idea (Discussions Ideas) → Show what you've made (Discussions Show-and-tell) → General discussion (Discussions General) |
||
|
|
1e9bcbf20d
|
fix(contributor-bot): serialize runs to avoid state.json races and duplicate cards (#1707) | ||
|
|
43b1b94c8e | Add preview release channel | ||
|
|
3fa12f71be
|
Add release preview workflow placeholder (#1705)
Some checks failed
ci / Packaged mac smoke (push) Blocked by required conditions
ci / Packaged windows smoke (push) Blocked by required conditions
ci / Detect PR change scopes (push) Failing after 11s
ci / Validate workspace (push) Has been skipped
nix-check / build (push) Failing after 2s
|
||
|
|
5b1e49ac8a | Add release preview workflow placeholder | ||
|
|
cba8bf151d | chore: align namespace lifecycle packaging | ||
|
|
6c16283850 |
Merge origin/main (post-7c8305f4) into reconcile branch
Brings in 10 new main commits: routine deep-link to specific conversations (#1508), Windows resource cache fix for Orbit templates, collapsible comment side panel (#1607), routines project radio polish, Copilot logo swap, and minor UI fixes. Conflicts resolved: - router.ts: garnet's home/view + marketplace routes + main's per-project conversationId deep-link field coexist on Route union - ProjectView.tsx: garnet's isPhantomDaemonRunMessage helper + main's isStoppableAssistantMessage helper both kept - ProjectView.run-cleanup.test.tsx: accepted HEAD (garnet's phantom-row regression test); main's three new tests for finalizeActiveAssistantMessagesOnStop / clearStreamingConversationMarker / shouldClearActiveRunRefs are queued as a follow-up TODO inline. |
||
|
|
2976c76fc3
|
test: expand Memory and Routines coverage (#1521)
* test: expand settings and packaged coverage * test: extend memory settings coverage * test: cover routine settings failure states * test: cover routine operation failures * test: fix daemon test typing on CI * test: decouple packaged smoke from orbit bug * test: avoid live memory LLM calls in route tests * test: fix daemon fetch typing in CI * fix: restore preview comment and inspect toggles * test: align manual edit flow with current inspector UX * test: align comment attachment flow with current preview comments UI * fix: probe resolved Codex launch path during detection * fix: remove duplicate board activation helper after rebase * test: update ghost cli detection mock * test: align FileViewer toolbar expectation * ci: move full app tests to extended lane * ci: run app tests by changed scope * ci: cover shared app inputs in test scopes * ci: avoid setup-node cache in windows packaged smoke * test: align extended settings and manual edit flows |
||
|
|
51d1c4e287
|
ci: skip upstream-only workflows on forks (#1586) | ||
|
|
ff569fa50c
|
feat(daemon): Critique Theater Phase 16 (M-phase rollout ratchet + /api/critique/conformance) (#1499)
* feat(web): pure reducer for Critique Theater states (Phase 7.1)
Pure CritiqueState reducer driven by the contracts-level PanelEvent
(the same shape both the live SSE stream and the recorded transcript
emit), so a single reducer powers both the in-flight panel and the
rerun replay. Lifecycle covers run_started → running → (shipped /
degraded / interrupted / failed), with panelist_open / dim /
must_fix / close / round_end events building per-round
CritiquePanelistView entries as they arrive.
Defensive behaviour that surfaced while writing the spec tests:
- Terminal phases (shipped / degraded / interrupted / failed) are
sticky against further lifecycle events for the same run, except
for parser_warning which can land late and is recorded in a side
channel without changing phase.
- A new run_started for a different runId at any time discards the
prior state and reboots, so the UI can launch consecutive runs
without an explicit reset action.
- Events whose runId does not match the active run return the same
state reference, so React's useReducer doesn't re-render
subscribers on stray traffic.
- Round bookkeeping keys by round number rather than "always last",
so an out-of-order panelist_dim for round 1 arriving after a
round 2 dim does not corrupt the round 2 bucket.
Test coverage: 18 cases covering each transition, the runId guard,
sticky-terminal behaviour, the out-of-order round invariant, and
the stable-identity guarantee. Sets up Phase 7.2 and 7.3 to wire
SSE + replay into the same reducer.
* feat(web): useCritiqueStream hook subscribes to SSE and feeds reducer (Phase 7.2)
createCritiqueEventsConnection is a pure connection manager that
mirrors apps/web/src/providers/project-events.ts: opens an
EventSource at /api/projects/:id/events, listens for every name in
CRITIQUE_SSE_EVENT_NAMES, decodes each frame back into a PanelEvent
(stripping the critique. prefix and merging the data payload), and
hands it to the caller's onEvent. Reconnect uses exponential
backoff (1s → 30s) and resets on `ready`; malformed payloads drop
with a dev-mode warning rather than tearing the stream.
useCritiqueStream wraps the manager in a useReducer that owns the
CritiqueState. enabled=false or a null projectId tears down the
connection cleanly; switching projectId closes the old connection
and opens a fresh one. The returned dispatch lets local UI
synthesise actions (e.g. an Esc keypress firing a synthetic
interrupted while a kill request is in flight); production traffic
comes from the SSE stream.
Test coverage:
- sse.test.ts (10 cases, node env): subscription set covers every
CRITIQUE_SSE_EVENT_NAMES channel; payload decoding lifts the wire
shape back to PanelEvent; malformed JSON is swallowed and does
not stop the stream; exponential backoff schedule and ready-reset
semantics are pinned with a setTimeout seam; close() cancels
pending reconnects and shuts the live source; no-op fallback
when EventSource is unavailable.
- useCritiqueStream.test.tsx (6 cases, jsdom env): idle pre-event,
reducer driven by synthetic actions, no connection when disabled
or projectId is null, clean close on unmount, projectId change
reopens cleanly.
* feat(web): useCritiqueReplay hook drives reducer from transcript file (Phase 7.3)
Fetches the per-run NDJSON transcript (one PanelEvent per line),
parses every line via the shared isPanelEvent predicate, and
dispatches into the same CritiqueState reducer the live SSE stream
uses. A single reducer means the UI rendering a replay can be
identical to the live panel, and a UI mounting both
useCritiqueStream and useCritiqueReplay in parallel does not have
to reconcile two state shapes.
speed knob is `paused | instant | live | { intervalMs: N }`.
- instant flushes every event synchronously, useful for opening a
finished run already at its terminal state.
- intervalMs paces dispatches at a fixed cadence so the reviewer
can watch the run unfold.
- paused parses the transcript but holds events back until the
caller advances speed (consumers can drive a scrubber later).
- live is reserved for the future "playback at original cadence"
feature, currently treated as instant; replay timestamps are not
yet persisted with each event so honest pacing requires a
follow-up Phase 7+ task.
gunzip seam handles `.ndjson.gz` transcripts via
DecompressionStream when present; the production fetch path picks
between text and arrayBuffer based on the URL extension. Both seams
are injectable so the unit tests don't need to spin up a real
network or a real gzip pipeline.
Test coverage (8 cases, jsdom env):
- Idle status before any URL is provided.
- speed=instant flushes the full transcript synchronously to
shipped state.
- speed={intervalMs:N} paces with the setTimeout seam, reaching
done after the last tick.
- speed=paused leaves status=playing with no dispatches.
- Empty transcript reports done with state still idle.
- Fetch rejection surfaces an error status with the message.
- Malformed NDJSON lines are skipped; valid events around them
still land.
- .gz transcripts route through the gunzip seam.
Closes the Phase 7 plan tasks 7.1 / 7.2 / 7.3 (reducer + stream +
replay), all on one branch ready for review. Phases 8+ (Theater
components) consume these from this PR.
* fix(web): close payload-override gap + paused-resume bug in Critique Theater hooks (Phase 7 review)
Two P1 fixes from lefarcen's review on PR #1307:
SSE payload override
`sseToPanelEvent` previously spread `data` after the channel-derived
`type`, so a payload-provided `type` could override the channel and
route a `critique.run_started` frame into the reducer as a `ship`
action. Reversed the spread so the channel-derived `type` is
authoritative, and revalidated the resulting object through the
contracts-level `isPanelEvent` predicate before returning. Frames
that fail validation (missing runId, empty runId, unknown type) are
dropped, so a malformed or compromised SSE frame can no longer
dispatch a wrong-shape action into the reducer.
Three new sse.test.ts cases pin the regression: hostile `type:'ship'`
in the payload still resolves to `run_started`, missing runId is
dropped, empty runId is dropped.
Replay pause/resume
`useCritiqueReplay` had one big effect keyed on `transcriptUrl`
only, so flipping `speed` from `paused` to `instant` never re-fired
and the held events sat undispatched. Split into a parse effect
(depends on URL, fetches and stores events in state) and a pace
effect (depends on parsed-events + speed, owns the cursor + timers).
The playback cursor lives in a ref that survives pause/resume
cycles, so flipping `paused` -> `instant` flushes from the current
position rather than restarting (which would double-dispatch
`run_started` and reset the reducer).
Two new useCritiqueReplay.test.tsx cases:
- paused-then-instant transitions from `playing` to `done` and
reaches the shipped terminal phase
- intervalMs paced playback dispatches one event, pauses to drain
the next scheduled timer, flips to instant, and confirms the
remaining transcript drains exactly once (cursor was preserved)
Doc consistency
The earlier source comment in useCritiqueReplay.ts claimed `live`
"paces by recorded timestamps" while the impl used zero-delay
timers and the PR body said it behaves like `instant`. Aligned to
reality: `live` currently behaves like `{ intervalMs: 0 }` (events
drain on successive microtasks via setTimeoutFn) because transcripts
do not yet carry per-event timestamps. Honest timestamp-driven
pacing is queued as a Phase 7+ follow-up.
Validated: pnpm guard, pnpm --filter @open-design/web typecheck,
Theater suite 47/47 (up from 42, +3 sse + 2 replay), full web suite
96 files / 888 tests.
* feat(i18n): seed Critique Theater key block (en + zh-CN; other locales fall back via spread)
* feat(web): Theater PanelistLane component (Phase 8.1)
* feat(web): Theater ScoreTicker component (Phase 8.2)
* feat(web): Theater RoundDivider component (Phase 8.3)
* feat(web): Theater InterruptButton component with Escape keybind (Phase 8.4)
* feat(web): Theater TheaterDegraded chip (Phase 8.5)
* feat(web): Theater TheaterCollapsed post-run summary (Phase 8.6)
* feat(web): Theater TheaterTranscript replay surface (Phase 8.7)
* feat(web): Theater TheaterStage top-level container (Phase 8.8)
* feat(web): Theater CSS using existing semantic tokens (no hex literals)
* feat(web): Theater public exports barrel
* fix(web): resolve P2 + P3 review feedback on Phase 8 (PR #1314)
Addresses all 4 P2 + 3 P3 items from codex, Siri-Ray, and lefarcen.
State-lifecycle fixes (3 x P2)
1. Reducer learns a synthetic `__reset__` action (`CritiqueResetAction`).
Host hooks dispatch it when their gating prop changes so a stale
run from a prior project / transcript cannot bleed into the next
context. Reset is idempotent on idle (returns the same reference).
2. `useCritiqueStream` dispatches `__reset__` at the top of its
connection effect, so a workspace switch from project A (which
streamed a critique) to project B clears the reducer before the
new EventSource opens. enabled=false also clears.
3. `useCritiqueReplay` dispatches `__reset__` at the top of its
parse effect, so transcriptUrl swaps (including swap-to-null after
a replay reached `shipped`) lift the reducer back to idle before
the new fetch starts.
SSE validation (1 x P2)
4. `sseToPanelEvent` now runs a per-variant `hasValidVariantShape`
check after the cheap `isPanelEvent` predicate. A
`critique.ship` frame missing `composite` / `round` / `status` /
`artifactRef` is rejected before reaching the reducer, so
TheaterCollapsed can no longer crash on `undefined.toFixed(1)`.
Every variant's required fields are validated: run_started
(protocolVersion, non-empty cast, maxRounds, threshold, scale),
panelist_* (round, role, plus variant-specific shape), round_end
(round, composite, mustFix, decision in {continue,ship}, reason),
ship (round, composite, status, artifactRef.{projectId,artifactId},
summary), degraded (reason, adapter), interrupted (bestRound,
composite), failed (cause), parser_warning (kind, position).
Reducer correctness (1 x P2)
5. `panelist_open` now materializes the round + an empty panelist
view (`{dims: [], mustFixes: []}`) so TheaterStage can highlight
the in-progress lane the instant the tag opens. Before this, a
stream that emitted only `panelist_open` after `run_started` left
`rounds = []` and the UI rendered no current round until a later
`panelist_dim` arrived.
Polish (3 x P3)
6. Brand role tint swaps from `var(--magenta, var(--accent))` to
`var(--purple, var(--accent))`. `--purple` is actually defined
across the design systems; `--magenta` is not, so Brand was
silently falling through to `--accent` and looking identical to
Designer.
7. New i18n key `critiqueTheater.interruptedSummary` for the
interrupted-collapse copy ("Interrupted at round N, best
composite X.X"). Previously the interrupted branch reused
`shippedSummary` and the UI read "Shipped at round..." for a run
that specifically did not ship. Native value in en + zh-CN; other
locales fall back via `...en` spread.
8. `TheaterDegraded` heading id comes from `useId()` instead of a
hardcoded `theater-degraded-heading`, so two chips rendered on
the same page (chat history with multiple completed runs) keep
their aria-labelledby references unambiguous.
Tests (15 new cases)
- reducer.test.ts (+5): __reset__ on running/terminal/idle, panelist_open materializes round, panelist_open does not stomp prior panelist data.
- sse.test.ts (+6): variant-level rejection for ship without required fields, degraded without adapter, run_started with empty cast, panelist_dim with non-numeric score, round_end with unknown decision, plus a positive fully-formed ship.
- useCritiqueStream.test.tsx (+2): state reset on projectId change, state reset on enabled flip false.
- useCritiqueReplay.test.tsx (+1): state reset on transcriptUrl swap to null after a replay reached shipped.
- TheaterCollapsed.test.tsx (text-pinning update): asserts the interrupted branch reads "Interrupted at round 1" + "best composite 7.9", and explicitly NOT "Shipped at round...".
- TheaterDegraded.test.tsx (+1): two chips on the same page get unique aria-labelledby ids that each resolve to an `<h3>`.
Validated
- pnpm guard clean
- pnpm --filter @open-design/web typecheck clean
- Theater suite: 13 files, 101 tests (was 86 on the first Phase 8 push, +15 new)
- tests/i18n/locales.test.ts 5 of 5 across 18 locales
* feat(web): CritiqueTheaterMount wires SSE + reducer into a single drop-in (Phase 9.1)
* feat(i18n): Critique Theater strings for de + ja + ko + zh-TW (Phase 9.2)
* fix(web): resolve P1 + P2 review feedback on Phase 9 (PR #1315)
Addresses every blocker from codex, Siri-Ray, and lefarcen. The
three state-lifecycle and SSE-validation issues they also flagged
inherit fixes from PR #1314's review pass that this branch now sits
on top of after rebase.
Real daemon kill on Interrupt (P1)
- CritiqueTheaterMount now POSTs to
/api/projects/:id/critique/:runId/interrupt alongside the
optimistic local dispatch. Before this fix, clicking Interrupt
only flipped the React state to interrupted while the daemon job
kept running. The fetch is best-effort: a 404 (endpoint not wired
yet, lands in Phase 15) is swallowed with a dev-mode console.warn
so the UI still moves to the collapsed badge.
- New fetchInterrupt test seam lets RTL assert on the URL / method
and simulate the "daemon not ready yet" path. Two tests pin both:
the happy URL proj-42/critique/run-abc/interrupt POSTs, and a
rejected fetch still flips the UI.
interruptPending reset on new run (P2)
- A ref-backed effect compares the current runId against the last
one we saw; when it changes, interruptPending is cleared. A user
who interrupts run-1 and then triggers run-2 from the same mount
now gets a fresh, enabled kill button instead of one stuck in
"Interrupting…". Pinned by a new mount test.
Escape keybind scope (P2)
- InterruptButton now checks the keydown target. Escape inside an
input, textarea, select, or contenteditable element is ignored
(and any ancestor of those via closest() is treated the same
way). Body-level focus still fires the keybind so the Theater
area's affordance keeps working. Four new tests cover textarea,
input, contenteditable, and the body-focus positive case.
userFacingName i18n key (P2)
- The spec at specs/current/critique-theater.md:6 mandates a single
critiqueTheater.userFacingName key so the "Design Jury" label can
be renamed without touching code. Phase 8 introduced
critiqueTheater.title by mistake; renamed across types.ts, en.ts,
zh-CN.ts, de.ts, ja.ts, ko.ts, zh-TW.ts, and the lone consumer
TheaterStage.tsx. The locale alignment test stays green.
Validated
- pnpm guard clean
- pnpm --filter @open-design/web typecheck clean
- Theater suite: 14 files, 112 tests (was 101 before, +11 new for
the Phase 9 review pass: 3 mount + 4 InterruptButton focus scope;
the rest were already in #1314's review fix).
- tests/i18n/locales.test.ts 5 of 5 across 18 locales.
* feat(daemon): adapter-degraded registry with TTL (Phase 10.1)
In-memory registry recording adapters that produced malformed or
oversize transcripts so the orchestrator can skip them for a TTL
window (default 24h) instead of cycling through known-bad providers
on every run.
Records carry reason (malformed_block | oversize_block |
missing_artifact), source label, and expiresAt. The test-only
clock seam lets the suite advance time deterministically and prove
that an expired entry stops counting as degraded without anyone
calling clearDegraded.
7/7 vitest cases green.
* feat(daemon): synthetic good + bad adapter fixtures (Phase 10.2)
Two test-only adapters that read the existing v1 transcript
fixtures (happy-3-rounds and malformed-unbalanced) and replay them
as either a full string or a 512-byte chunked stream. The chunked
form is what the conformance harness uses to prove the parser
holds together when the transcript arrives in arbitrary network
slices, not as one buffered blob.
* feat(daemon): adapter conformance harness (Phase 10.3)
runAdapterConformance pulls a transcript through the same
parseCritiqueStream pipeline the orchestrator uses and classifies
the outcome as shipped, degraded, or failed. On a degraded
outcome it forwards the matched reason to the adapter-degraded
registry, so a single nightly conformance run is what populates
the skip list rather than the orchestrator learning each adapter
is broken at request time.
5/5 vitest cases green covering shipped, malformed degraded,
oversize degraded, no-ship failure, and the harness-thrown
failure path.
* test(e2e): Critique Theater Playwright suite (Phase 11)
Six tests, one viewport per visual case, deterministic SSE
fixtures stubbed via page.route(). Adds the suite to
test:ui:extended so the existing extended-UI lane picks it up.
Coverage:
1. Happy path: a single mounted theater plays the full
fixture (1 run_started, 5 panelists open / dim / must_fix /
close, 1 round_end, 1 ship) and ends on the score badge.
2. Interrupt mid-run: the panelist that is open at the time
the interrupt button is clicked closes with an interrupted
marker and the transcript freezes there.
3. Visual regression at 375x720 mobile.
4. Visual regression at 768x1024 tablet.
5. Visual regression at 1280x800 desktop.
6. A11y role tree: the theater region exposes a labelled
landmark, each panelist lane is a group with an accessible
name, the score is a status live region.
All SSE traffic is stubbed by page.route so the suite runs in CI
without a daemon. The toggle is seeded via localStorage by
bootAppWithCritiqueEnabled so the gate behaves as if Settings
flipped it on. typecheck clean; playwright --list reports 6.
* test(web): reducer p99 bench at 10k iterations (Phase 13.1)
Locks the documented 2ms budget for the Critique Theater reducer
on a representative SSE script (27 actions, one full happy run)
behind a regression gate. Asserts p99 stays under 4ms (2x the
documented budget) so CI runners with a noisy neighbour do not
flake while a real regression to 20ms or 200ms still trips.
The bench is a vitest case rather than a bare microbenchmark so
it runs in the same CI lane as every other web test and does not
need a parallel runner.
* test(web): critique surface coverage walker (Phase 13.2)
Walks the public critique surface (11 SSE event names, 5 panelist
roles, 6 lifecycle phases, 9 named i18n keys) and asserts each
named symbol appears in both the src corpus and the test corpus.
The walker is the gate that catches a rename in one half of the
codebase without a matching update in the other half: a future
PR that drops 'panelist_must_fix' from the reducer without also
removing its test reference fails this suite.
62 assertions, one per symbol per corpus.
* docs: Critique Theater user guide (Phase 14.1)
Seven sections aimed at end users (not contributors):
1. What is Design Jury
2. How it works (the five panelists, auto-converging rounds,
the composite formula)
3. Settings (the M1 toggle and what it does)
4. Reading the score badge
5. Replay surface
6. Troubleshooting (degraded, interrupted, failed)
7. FAQ
The composite formula is documented as
designer * 0 + critic * 0.4 + brand * 0.2 + a11y * 0.2 + copy * 0.2
because anyone trying to reverse-engineer the score is going to
search for those weights and the docs are the place they should
land first.
* docs(daemon): critique module AGENTS map (Phase 14.2)
Daemon-side wayfinder for the apps/daemon/src/critique directory.
Tables every file, what owns what invariant, and the 'when you
change anything here' guide so a future contributor does not
have to reverse-engineer the rollout resolver before adding a
new SSE event.
* docs(web): Theater module AGENTS map (Phase 14.3)
Web-side mirror of the daemon AGENTS map. Same file table, same
invariants section, same change-impact guide, sized to the
Theater component package.
* feat(daemon): rollout flag resolver (Phase 15.1)
Single decision point every caller consults to know whether the
orchestrator should wire the critique pipeline for a given run.
Priority:
1. Skill-level policy (required wins, opt-out wins inversely)
2. Per-project override from the Settings toggle
3. OD_CRITIQUE_ENABLED env override
4. Rollout phase default
M0 dark-launch false
M1 settings only false (toggle is off until the user flips it)
M2 per-skill true if skill opted in
M3 global default true
OD_CRITIQUE_ROLLOUT_PHASE parser defaults to M0 on unknown input
so a fresh install never surprises a user with the feature on.
10/10 vitest cases green covering every cell of the matrix.
* feat(web): Settings toggle hook for Critique Theater (Phase 15.2)
React hook that reads critiqueTheaterEnabled from the existing
open-design:config localStorage blob and stays in sync via:
- the platform storage event (cross-tab)
- a open-design:critique-theater-toggle CustomEvent (same-tab)
Same-tab event is the one that fires when the Settings panel saves
in the current window: the toggle and every mounted theater update
without a page reload.
setCritiqueTheaterEnabled(next) is the imperative setter the Settings
panel calls. It preserves the rest of the stored config (mode, apiKey,
etc.) and dispatches the same-tab event after the localStorage write.
The web hook reflects what the user toggled; the daemon-side
isCritiqueEnabled is the final routing authority (project override,
env, rollout phase). When they disagree, the daemon wins for backend
gating and the web reflects the toggle state.
6/6 vitest cases green covering first read, stored read, same-tab
event flip, config preservation, corrupted JSON tolerance, and
cross-tab storage event.
* test(web): Phase 15 toggle hook failure-mode coverage (PR #1320)
lefarcen P2 on PR #1320 flagged that the PR body claimed safe
behavior for disabled localStorage, non-object JSON, and missing
CustomEvent shim, but the suite only covered corrupt JSON plus
happy-path storage events. Added four failure-mode tests so the
swallowed errors are not silently traded for a throw in a future
refactor:
1. Returns false on a stored JSON value that parses to an array
(non-object). Catches a regression where the guard treats
anything truthy as a config blob.
2. Returns false on a stored JSON value of literal 'null'.
typeof null === 'object' in JS, so the guard has to check null
explicitly; this test pins that check.
3. Returns false when localStorage.getItem throws (private mode /
disabled storage / SecurityError). The hook must swallow and
return false so the rest of the app keeps rendering.
4. setCritiqueTheaterEnabled still dispatches the same-tab
CustomEvent when localStorage.setItem throws (quota exceeded /
disabled storage). The dispatch path is the in-session
broadcast that keeps every mounted hook coherent even when
persistence is unavailable; verified by mounting two probes
and asserting both flip after the setter is called with a
throwing setItem.
10/10 vitest cases green (6 existing + 4 new).
* fix(web): honor CustomEvent payload in toggle hook listener (PR #1320)
Both Siri-Ray (blocking) and lefarcen (P2 new) caught the same
real bug in the failure-mode test I added in
|
||
|
|
53997990b7 |
Merge origin/main (post-0.7.0) into reconciled garnet branch
Second-pass merge layering 41+ new commits from origin/main on top of the first reconcile commit. Headline upstream additions absorbed: - 0.7.0 release: redesigned chat bubble user-text styling, neutralised palette, lucide icons, ElevenLabs audio voice option discovery in the prompt composer, analytics tracking (PostHog) wired across home / studio / create surfaces, Prometheus `/api/metrics` endpoint, critique-theater drop-in mount with a settings toggle. - Misc upstream fixes (titlebar padding, release header layout, deck preview chrome, feedback form auto-scroll, conversation-created SSE on routine runs, etc.) Conflict resolutions (12 files, ~22 hunks): - contracts barrel + prompts/system: union of both sides; new analytics exports (`./analytics/events`, `./analytics/public-params`) added alongside garnet's plugin/atom/genui exports. Both ElevenLabs voice fields (audioVoiceOptions/audioVoiceOptionsError, main) and pluginBlock/activeStageBlocks (garnet) preserved on ComposeInput. - daemon/server.ts: Prometheus `/api/metrics` route inserted after garnet's `/api/daemon/shutdown`. main's `createAnalyticsService` call added before the chat-run service init alongside the prior reconcile note about the dropped legacy POST /api/projects body. - App.tsx: handleCreateProject now consumes both garnet's plugin fields (pluginId / appliedPluginSnapshotId / pluginInputs / autoSendFirstMessage) and main's analytics requestId. Tracking fires success + failure paths; PluginLoopHome auto-send sessionStorage flag is preserved. - ProjectView.tsx: the garnet auto-send useEffect coexists with main's `useCritiqueTheaterEnabled()` hook. - ChatComposer.tsx: imports merged (drop now-unused fetchSkills, add analytics provider + tracking + buildVisualAnnotationAttachment). - index.css: main's redesigned `.msg.user .user-text` chat bubble styling wins over garnet's plain text rule; garnet's `.msg-plugin-chip*` rules preserved alongside. - EntryView.tsx: accepted HEAD (garnet wrapper) — consistent with reconcile decision #2. main's added PetRail / TopTab / analytics view tracking is intentionally NOT brought into the wrapper; the follow-up to re-integrate PetRail / image-templates / video-templates into EntryShell still stands and now also covers analytics view-tracking hooks. - daemon/package.json + pnpm-lock: merged dep set (tar + posthog-node + prom-client coexist). - Test fixtures (FileWorkspace.test): kept garnet's plugin-folders describe block intact; main's projectKind="prototype" addition is dropped where it conflicted with garnet's plugin-folder fixture files. Verification: `pnpm install` (after lockfile reconciled), `pnpm typecheck` exits 0 across all workspace packages. Follow-up not done in this commit: - PetRail / image-templates / video-templates / 0.7.0 analytics view-tracking hooks need to be added to EntryShell. - Critique-theater settings toggle UX (added on main) lives in the SettingsDialog hierarchy; the reconcile state preserves the SettingsDialog so this should work without changes, but no end-to-end verification yet. |
||
|
|
d3602be666 |
Merge origin/main into garnet-hemisphere (reconcile)
Merge of `origin/main` (`03ed3960`, 2026-05-13 pre-0.7.0) into the 161-commit garnet-hemisphere line, reconciling the product-vibe-coded plugin/marketplace/EntryShell surfaces from garnet with the routines / skills / live-artifacts feature work landed on main since the fork point. Headline decisions (full rationale + side-by-side screenshots in `specs/change/20260513-garnet-skills-automations/reconcile-result-vs-garnet.md`): - #1 SettingsDialog: keep main's Memory / Skills / External MCP / Connectors / Routines / MCP server nav items even though the top-level /integrations + /automations routes also cover them. Two entries coexist for now; revisit once Track A/B fill in the placeholder content. - #2 EntryView: accept garnet's thin wrapper delegating to EntryShell. Main's PetRail sidebar + image-templates/video-templates tabs are intentionally deferred to a follow-up that re-integrates them into the new EntryShell layout. - #3 /integrations + /automations top-level routes: kept (garnet's product intent). Skills tab is still a "Coming soon" placeholder awaiting Track A; Routines/Schedules/Live-artifacts cards on /automations are still mock awaiting Track B. - #5 DesignFilesPanel: hybrid — main's pagination as primary list, garnet's Plugin folders section preserved between the live-artifacts block and the pagination block. (by-kind sections drop in favour of pagination; plugin-folders rendering stays because it is a garnet-specific product addition.) - #7 server.ts (10 hunks, ~5400 conflict lines): manual hunk-by-hunk merge. Both daemon admin routes + plugin/genui routes (garnet) and routines/memory/skills upgrades (main) preserved. Garnet's inline project route block kept alongside main's `registerProjectRoutes` / `registerProjectUploadRoutes` modular wiring — duplicate route audit is a follow-up. Garnet's POST /api/projects plugin-snapshot resolution + default-scenario fallback is intentionally dropped from the inline body (now handled by registerProjectRoutes) and listed for follow-up re-integration into `project-routes.ts`. Verification (worktree at /Users/elian/Documents/open-design-garnet): - `pnpm typecheck` exits 0 across all workspace packages - daemon (`pnpm tools-dev run web --namespace reconcile-shots`) boots, serves `/api/daemon/status` healthy, and survives a Playwright walkthrough of /integrations / /automations / home / projects / design-systems / plugins / settings dialog - `@open-design/plugin-runtime` package built (was missing dist/ on garnet); without it the daemon's plugins/* imports fail at boot Track A (Skills tab → real SkillsSection) and Track B (Automations cards → real routines / live-artifacts backend) are the two remaining follow-ups blocking the placeholder/mock content from going live. See `spec.md` and `track-skills.md` in the same directory. |
||
|
|
5172e37217 |
Merge origin/main into release/v0.7.0 to prepare merge-back PR
Resolves 7 conflicts via hybrid strategy: - apps/web/src/components/EntryView.tsx: take main (Discord+X pills are forward feature) - apps/web/src/components/Icon.tsx: take main (switch-case refactor) - apps/web/src/components/NewProjectPanel.tsx: take release (preserve #1514 dropdown UX validated in 0.7.0 acceptance) - apps/web/src/index.css: take main (project-target-platforms / instructions chip styles) - apps/web/tests/components/FileViewer.inspect-empty-hint.test.tsx: accept main's deletion - nix/package-daemon.nix, nix/package-web.nix: take main pnpmDepsHash Non-conflicting hunks from #1519 (AppChromeHeader), #1428 (PostHog analytics call sites), and #1540 (release light background) are preserved via auto-merge. |
||
|
|
6341b2677a
|
docs(pr): require user-perspective description and surface area (#1520)
* docs(pr): require user-perspective description and surface area The previous template asked for Summary + Validation, which encouraged code-perspective descriptions and let user-visible surface changes slip past review unnoticed. Replace with: - "Problem" — issue link + motivation - "What users will see" — first-person user-visible effect - "Surface area" — 9-item checklist (UI, shortcut, CLI/env, API/contract, extension point, i18n, top-level dependency, default behavior change, none) - "Screenshots" — required when UI surface is checked, focused on the entry point users discover rather than the feature in isolation - "Validation" — kept, retitled away from "Summary" Authoritative rules added to AGENTS.md under a new "Pull request expectations" section so external contributors' agents (Claude Code, Cursor, etc.) pick up the requirement when reading the repo. CONTRIBUTING.md gets one pointer line in "Commits & pull requests"; localized CONTRIBUTING variants (zh-CN, de, fr, ja-JP, pt-BR) are left for follow-up translation PRs per the existing docs-update workflow. The existing "Fixes #" prompt is preserved verbatim — that template addition from #1263 enforces PR-to-issue auto-linking and stays load-bearing. * docs(pr): broaden dependency surface to dev deps as well The "New top-level dependency" checkbox narrowed scope to runtime deps, but CONTRIBUTING.md L239 says "No new top-level dependencies" without limiting to runtime, and the AGENTS.md rule uses the same broad phrase. A new devDependency (tool/test/build package) belongs in the same bytes-vs-benefit explanation, so the checklist item should match. * docs(pr): add Why and Bug fix verification; scope deps to root package.json Three follow-up tweaks from review feedback: 1. Rename `## Problem` to `## Why` with a broader prompt that asks contributors to cover both their own use case (what made them write this PR today) and the pain being addressed. The old "Problem" framing only covered user-facing motivations and left no slot for the contributor's stake — a key signal for triaging external PRs. 2. New `## Bug fix verification` section between Screenshots and Validation, conditional on the PR being a bug fix. Surfaces the AGENTS.md "Bug follow-up workflow" red-spec requirement at PR-authoring time instead of leaving it implicit; asks for the test path and the red-on-main / green-on-branch confirmation. 3. Clarify the "New top-level dependency" checkbox to specify the **root** `package.json`. Without that word, contributors in a monorepo could read the check as applying to any workspace `package.json` (e.g. adding `react` to `apps/web/package.json` would be in scope) when CONTRIBUTING.md L239's "small on purpose" rule clearly meant root-level deps only. AGENTS.md `## Pull request expectations` and CONTRIBUTING.md's pointer line are updated to match the new section names and add the bug-fix red-spec expectation. |
||
|
|
e1bc83a476
|
feat(analytics): PostHog product analytics (P0 events, consent-gated, packaged) (#1428)
* feat(analytics): scaffold PostHog product-analytics integration
- Add @open-design/contracts/analytics subpath with the 17 P0 event
payload types, header constants, and code↔CSV enum mapping helpers.
- Add apps/daemon/src/analytics.ts with env-gated posthog-node client,
request-scoped analytics context reader, and artifact-id anonymizer.
- Expose GET /api/analytics/config so the web bundle never embeds the
PostHog key at build time; daemon owns POSTHOG_KEY / POSTHOG_HOST.
- Add apps/web/src/analytics module (identity + lazy posthog-js client
+ React provider) and mount it under <I18nProvider> in app/layout.
No event wiring yet — that lands in the next commit alongside trigger
points (App.tsx, EntryView, NewProjectPanel, SettingsDialog, FileViewer,
runs.ts).
* feat(analytics): wire app_launch, home_view, home_click, project_create_result
- App.tsx: fire app_launch once after first effect tick. handleCreateProject
now emits project_create_result on both success and failure paths.
- EntryView.tsx: home_view (page) gated on agents loading so
has_available_cli isn't transiently false; home_view (asset_panel) fires
per top-tab change with the right result_count.
- NewProjectPanel.tsx: home_click create_button fires before delegating to
the parent; a fresh request_id is generated here and threaded through
onCreate so the matching project_create_result stitches via $insert_id.
- contracts/analytics: tighten createTabToTracking and topTabToTracking
for the worktree branch's renamed tabs (live-artifact, templates).
* feat(analytics): wire settings_view + 3 settings_click events
- settings_view fires on dialog mount and on every section switch,
carrying the active section (mapped via settingsSectionToTracking
for the 16-section worktree layout), execution_mode, and the
selected CLI provider id when present.
- settings_click execution_mode_tab: setMode now emits before/after
values whenever the user toggles between Local CLI and BYOK.
- settings_click cli_provider_card: agent card onClick reports
cli_provider_id via agentIdToTracking (kiro → other).
- settings_click byok_field: onFocus added to api_key, model select,
and base_url inputs; provider_id widened to include google so the
worktree's Gemini protocol slot type-checks.
* feat(analytics): wire studio_view + studio_click chat, studio_view artifact
- packages/contracts/src/analytics/artifact-id.ts: FNV-1a 64-bit helper
produces a 16-hex anonymized id for (projectId, fileName). Stable
cross-platform so the daemon and the web bundle resolve the same id
without a Web Crypto round-trip; daemon now re-exports it.
- ChatComposer: studio_view chat_panel fires once per project mount,
studio_click chat_composer fires on attachment + send buttons with
estimated user_query_tokens (length/4) and has_attachment.
- FileViewer: studio_view artifact fires once per (project, file) at
the dispatcher level, before any sub-viewer renders, with
artifact_kind derived from the renderer registry / file.kind table.
- Widen TrackingExportFormat to include markdown and cloudflare_pages
so the worktree branch's full share menu can emit verbatim.
* feat(analytics): wire studio_click share_option + artifact_export_result
HtmlViewer's share menu now emits both events per click via a
fireShareExport helper:
- studio_click share_option fires immediately on click with the chosen
export_format and a fresh request_id.
- artifact_export_result fires when the export resolves — success for
sync exporters (html, markdown, template) the moment the call
returns, success/failed for async exporters (pdf, zip, deploy)
via .then/.catch. The same request_id threads both events so
PostHog stitches click → result via $insert_id.
DEPLOY_PROVIDER_OPTIONS maps to the CSV's vercel / cloudflare_pages
slots; markdown is now a first-class export_format value.
Also ignore .env.local so local POSTHOG_KEY / .env-style secrets
don't get committed.
* feat(analytics): emit run_created and run_finished from the daemon
POST /api/runs now reads the analytics context off the
x-od-analytics-* headers the web client sets on every fetch, then:
- Captures run_created with project_id, conversation_id, run_id,
model_id, agent_provider_id (mapped via agentIdToTracking),
skill_id, design_system_id, plus the token_count_source marker.
- Schedules a run_finished capture on runs.wait(run) resolution,
mapping succeeded/canceled/failed to success/cancelled/failed and
reporting total_duration_ms.
Both events use a stable insert_id derived from the same uuid so
PostHog dedupes the daemon-side mirror against any future
web-side capture without double-counting.
Token sub-fields (user_query_tokens/system_prompt_tokens/...) stay
omitted in v1 — the claude-stream parser only exposes input/output
totals today. See tracking-doc-issues.md §3.2.
* feat(analytics): emit settings_cli_test_result + settings_byok_test_result
The original BLOCKING-list assumed these CSV P0 events were not
implementable in this branch because main lacked Test buttons. The
worktree HEAD actually wires `handleTestAgent` and `handleTestProvider`
in SettingsDialog, so both events are now in scope.
- handleTestAgent emits settings_cli_test_result on success and
failure paths with cli_provider_id mapped via agentIdToTracking,
result drawn from result.ok / catch branch, error_code from
result.kind or the thrown error name, and duration_ms timed via
performance.now().
- handleTestProvider emits settings_byok_test_result analogously,
using apiProtocol (anthropic|openai|azure|ollama|google) directly
as provider_id — wider than the CSV's 5-value enum, documented in
tracking-doc-issues.md §2.5.
Contracts: add SettingsCliTestResultProps / SettingsByokTestResultProps
plus matching track* helpers. AnalyticsEventName union now covers all
14 P0 events this branch supports.
* feat(analytics): gate PostHog on the existing telemetry.metrics consent
The integration now reuses the same first-launch privacy banner +
Settings → Privacy toggle that gates Langfuse, so a single user
decision controls both telemetry sinks.
- /api/analytics/config now consults the persisted AppConfigPrefs:
it returns enabled=true only when POSTHOG_KEY is set AND the user
has chosen "Share usage data" (telemetry.metrics === true). The
response also echoes installationId so the web client uses the
same anonymous id Langfuse keys off of — one identity per install,
shared across both sinks.
- Web AnalyticsProvider:
- Bootstrap fetch resolves installationId and threads it through
the x-od-analytics-anonymous-id header on every /api/* fetch,
so daemon-side captures (run_created / run_finished /
project_create_result) land on the same person record.
- Exposes a setConsent(granted) method that calls posthog-js's
opt_in_capturing / opt_out_capturing, wired from App.tsx via a
useEffect watching config.telemetry?.metrics. Toggling Privacy
→ metrics now stops/resumes events immediately, no reload.
- app_launch additionally gates on telemetry.metrics so a freshly-
declined user fires nothing, and a freshly-opted-in user fires on
the next reload.
* feat(packaging): bake POSTHOG_KEY into packaged daemon spawn env
Wires PostHog product analytics through the same Langfuse-style build-
secret pipeline so official Open Design builds ship with the key while
fork builds compile without it (the integration short-circuits cleanly
when POSTHOG_KEY is absent).
tools/pack
- resolveToolPackConfig reads POSTHOG_KEY / POSTHOG_HOST from
process.env at packaging time, validates them (no whitespace in the
key, http(s) URL for host, trailing-slash strip), and stamps them on
ToolPackConfig. Fork builds without the env vars simply omit the
fields; the daemon-side gate keeps things off in that case.
- Mac, Windows, and Linux packaged-config writers each append the two
fields to open-design-config.json next to the existing
telemetryRelayUrl entry.
apps/packaged
- RawPackagedConfig / PackagedConfig surface posthogKey / posthogHost
so the Electron entry and headless entry both forward them to the
daemon sidecar.
- buildPackagedDaemonSpawnEnv emits POSTHOG_KEY / POSTHOG_HOST into
the daemon child env when present. The daemon's existing analytics
module reads these via process.env — no daemon-side changes needed.
- The headless packaged path falls back to process.env for fields the
builder hasn't injected, mirroring how OPEN_DESIGN_TELEMETRY_RELAY_URL
is read there.
CI
- release-beta.yml and release-stable.yml expose POSTHOG_KEY (secret)
and POSTHOG_HOST (var) at workflow-env scope so every packaging job
inherits them. PR / fork builds without these set simply skip the
bake step.
Tests
- tools/pack: config.test.ts covers bake-through, fork-build omission,
whitespace rejection, invalid-URL rejection, and trailing-slash
normalization.
- apps/packaged: sidecars.test.ts covers buildPackagedDaemonSpawnEnv
forwarding the keys when present and omitting them when null.
* feat(analytics): enable PostHog autocapture + perf + exceptions
Flip on the PostHog SDK's automatic diagnostic features so we capture
click paths, page transitions, web vitals, dead clicks, and browser
exceptions without scattering instrumentation through the codebase.
Privacy defense lives in one place — apps/web/src/analytics/scrub.ts —
wired in via posthog-js's `before_send` hook so every outgoing event
passes through the same audit point:
- $autocapture / $rageclick / $dead_click / $copy_autocapture:
strips $el_text and value/placeholder/aria-label attrs from any
input, textarea, password input, or contenteditable element. PostHog
autocapture does not capture input.value by default, but $el_text
on a <textarea> reflects the typed content — that's the prompt
body for us, so it has to be scrubbed every time.
- $pageview / $pageleave: drops query string and fragment from
$current_url / $referrer so any future ?q=… can't leak.
- $exception: rewrites file:// and absolute filesystem paths in
stack frames to app://apps/<repo-relative> so we don't ship the
user's home directory.
- Suppresses $opt_in entirely — duplicate of our explicit
setConsent toggle in App.tsx.
Element-level defense in depth is limited to the single most sensitive
surface: the chat composer textarea gets `ph-no-capture` so PostHog
never even generates an event for clicks inside that subtree. Every
other input relies on scrub.ts — sprinkling the class through every
form would be noisy and easy to forget on new surfaces.
The existing Privacy → "Share usage data" toggle continues to gate
every new feature: posthog-js's opt_out_capturing() halts autocapture,
$pageview, $exception, web vitals, and dead clicks alongside the
explicit capture() calls — one global switch.
11 unit tests pin the scrub rules in apps/web/tests/analytics-scrub.test.ts.
* ci(nix): bump pnpmDepsHash for posthog-js + posthog-node additions
Adding posthog-js to apps/web and posthog-node to apps/daemon changed
pnpm-lock.yaml, which Nix's fixed-output pnpmDeps derivation pins by
sha256. The CI nix flake check failed with:
specified: sha256-KF3Mld72/iau+pJmA7HvnanRx8VLtDP0N624SKrtrrc=
got: sha256-PGFgX4lYyeH2TRAXfUq52A3EOa6bb1gO59hPsXhEk3s=
Copy the new hash into both nix/package-web.nix and
nix/package-daemon.nix per the procedure documented in nix/README.md
§"First-build hash pinning".
* feat(analytics): unify PostHog identity with Langfuse installationId
PostHog's distinct_id is the installationId stamped by /api/analytics/
config; Langfuse already reads the same id off app-config.json to
populate trace.userId. With both sinks keying off the same anonymous
identity, dashboards can correlate user actions (PostHog events) with
LLM runs (Langfuse traces) without re-identifying.
Two gaps closed:
1. applyConsent(false) — clear posthog-js's persisted ph_*_posthog
localStorage entry on opt-out via posthog.reset(). Without this, a
user who opts out, then clicks Delete my data, then re-opts in
would see PostHog stitch their new session to the deleted identity
because bootstrap.distinctID only takes effect on first init.
2. applyIdentity(newInstallationId) — Delete my data rotates the
installationId in app-config; App.tsx now watches config.installationId
and calls posthog.reset() then identify(newId) so the next event
batch is fully decoupled from the deleted one. Idempotent on
same-id re-renders so benign config refreshes don't churn PostHog
identities.
The fetch wrapper's x-od-analytics-anonymous-id header also flips to
the new id on rotation so daemon-side captures (run_created /
run_finished) land on the same person record from the very next API
call, not after a reload.
The end-to-end rotation flow is verified against a live PostHog
project; these unit tests pin the safety guards (no-client paths, null
inputs) since stubbing posthog-js's init-loaded callback chain is
brittle.
* fix(langfuse): require both metrics AND content consent for trace reports
Tightens the Langfuse gate so a user who shares anonymous metrics but
NOT conversation content stops emitting Langfuse traces entirely —
Langfuse is used for turn-quality evals which only make sense with
prompt/output bodies. PostHog (product analytics, content-free) stays
gated on `metrics` alone and is unaffected.
i18n: "Conversation content" → "Conversation and tool content" with
hints expanded to mention tool inputs/outputs so the consent surface
matches what the trace actually carries (en + zh-CN).
Bundled here per PR scope — change originated outside this PostHog
PR but lands cleanly on the same files; gating Langfuse strictly
on `content` makes the dual-sink consent model (PostHog = metrics,
Langfuse = metrics + content) symmetric across both i18n locales and
the daemon-side gate.
* feat(analytics): wire byok_provider_option + fix PR review P1s
Adds the BYOK protocol-chip click event (5-value provider_id mirroring
the apiProtocol Settings UI) and resolves four P1 review threads on
PR #1428.
byok_provider_option:
- New SettingsClickByokProviderOptionProps in contracts (provider_id =
anthropic|openai|azure|google|ollama; maps to CSV's 5 values per
tracking-doc-issues.md §2.5).
- trackSettingsClickByokProviderOption helper in apps/web/src/analytics.
- SettingsDialog hooks it on the protocol-chip onClick alongside the
existing setApiProtocol call; is_selected reflects whether the chip
was already active.
Review fixes:
1. client.ts (Siri-Ray): clear `initPromise` when the resolution is
null so a Privacy → metrics opt-in after a previous decline triggers
a fresh /api/analytics/config fetch. Without this, the disabled
response was cached forever — first-session opt-in needed a reload
to start sending PostHog events.
2. provider.tsx (Siri-Ray): replace `url.includes('/api/')` with a
strict same-origin + /api/ pathname check (shared
`isSameOriginApiCall` helper). Outbound third-party URLs containing
`/api/` (e.g. provider.example.com/api/x) no longer receive our
x-od-analytics-* headers.
3. provider.tsx (codex-connector, lefarcen): gate header injection on
`resolvedAnonId` being non-null. When Privacy → metrics is off,
/api/analytics/config returns enabled=false → resolvedAnonId stays
null → wrapper never installs → daemon can't read consent-bearing
headers → no daemon-side PostHog event. setConsent now also clears
resolvedAnonId on opt-out and re-fetches on opt-in.
4. daemon/analytics.ts (defense in depth): createAnalyticsService now
takes dataDir and capture() re-reads app-config to check
telemetry.metrics inside the fire-and-forget wrapper. Even if a
stale header somehow reaches the daemon after opt-out, the capture
is dropped before posthog-node.capture is called.
* fix(web): place "Share usage data" on the right in privacy consent banner
Swap button order in PrivacyConsentModal and the in-settings ConsentCard
so the affirmative "Share usage data" lands on the right and "Not now"
on the left. Matches the OK-on-the-right pattern users expect for
primary actions.
Both buttons keep equal visual prominence (same .privacy-consent-action
styling) so the swap doesn't change the EDPB equal-prominence stance
called out in the original Langfuse telemetry spec.
* feat(analytics): populate run_finished token totals from claude-stream usage
Daemon's claude-stream parser already emits agent usage events with
input_tokens / output_tokens totals; the run service buffers them in
run.events and Langfuse reads them out the same way. The run_finished
PostHog event was leaving these fields empty.
Scan run.events for the most recent agent usage frame on terminal
transition and emit input_tokens / output_tokens / total_tokens when
present. token_count_source flips to 'provider_usage' only when at
least one count landed; runs without provider-side usage data keep
'unknown'.
Provider does not break the input down into the 7 sub-fields the
tracking doc lists (memory / context / attachment / system_prompt /
…); those stay omitted until a parser change exposes them.
* feat(analytics): estimate user_query_tokens from prompt length
The user_query_tokens field for run_created / run_finished was hardcoded
to 0. We can't tokenize without bundling a model-specific tokenizer, but
the character/4 heuristic is the industry-standard estimate when one
isn't available and is enough for funnel analysis (prompt-length cohorts,
short-vs-long-query conversion rates).
Extracted from req.body via the same telemetryPromptFromRunRequest
pattern the daemon already uses for langfuse-bridge (currentPrompt then
message fallback). Only the integer count goes to PostHog — the prompt
text itself never leaves the daemon.
token_count_source flips appropriately:
- run_created with a prompt: 'estimated' (was 'unknown')
- run_created with no prompt: 'unknown'
- run_finished with provider usage: 'provider_usage' (overrides
baseProps' 'estimated' value)
- run_finished without provider usage: inherits 'estimated' or 'unknown'
from baseProps so input/output absent doesn't mask the estimate.
|
||
|
|
5077a1cd38
|
feat(landing-page): split catalog into per-facet pages + auto-deploy on content changes (#1158)
* feat(landing-page): split catalog into per-facet pages + auto-deploy on content changes
Convert the single-page landing into a content-driven multi-page site
sourced directly from the canonical Markdown bundles in the repo root,
and close the deploy loop so contributor edits go live without manual
follow-up.
## What's new
- `/skills/`, `/systems/`, `/craft/`, `/templates/` index + detail
pages, generated from `skills/<slug>/SKILL.md`,
`design-systems/<slug>/DESIGN.md`, `craft/*.md`, and
`templates/live-artifacts/<slug>/README.md` via Astro content
collections (`app/content.config.ts`). No mirroring of content into
the landing-page package — `glob` re-scans on every build.
- Faceted sub-routes generated from frontmatter:
- `/skills/mode/<slug>/` — 8 pages (deck, prototype, image, …)
- `/skills/scenario/<slug>/` — 18 pages after alias collapse
- `/systems/category/<slug>/` — 21 pages
Each page owns its own `<title>`, meta description, and
`CollectionPage` JSON-LD; chips on the parent index pages are now
real anchors that link to these facet routes.
- Updated top-bar nav (`_components/header.tsx`) to point at the new
internal routes with live counts pulled from the catalog. Counts in
the homepage hero meta description likewise driven by
`getCatalogCounts()` so they never drift.
- Per-skill / per-template thumbnails. A Playwright generator
(`scripts/generate-previews.ts`) walks every `example.html` and
`templates/live-artifacts/<slug>/index.html`, screenshots them at
1440×900@2x, and writes PNGs to `public/previews/`. The catalog
data layer auto-detects presence and degrades gracefully when an
artifact has no renderable HTML.
## Plumbing the auto-update loop
- `landing-page-deploy.yml` and `landing-page-ci.yml` now trigger on
changes under `skills/`, `design-systems/`, `craft/`, and
`templates/`. Without this, a contributor adding a new SKILL.md to
`main` would silently skip the deploy and the published site would
fall behind.
- Both workflows now install Playwright Chromium (cached by version)
and run `pnpm previews` before `astro build`, so generated
thumbnails ship in `out/previews/` automatically. Preview generation
is `continue-on-error: true` — a single broken example.html should
not block the deploy of the rest of the catalog.
- `apps/landing-page/public/previews/` is gitignored: the directory
is owned by CI and would otherwise add ~70MB of binary churn to the
repo on every regeneration.
## Tag canonicalization
- `app/_lib/catalog.ts` adds a small per-scope alias table so
authoring drift like `od.scenario: operation` vs `operations`, or
`live` vs `live-artifacts`, collapses to a single canonical route
instead of leaking two near-empty pages. Mode and category alias
tables are scaffolded but currently empty.
## Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors,
0 warnings, 0 hints across 25 Astro files
- `pnpm --filter @open-design/landing-page build` — 341 pages built
(1 home + 8 mode + 18 scenario + 21 category + N detail pages +
sitemap + RSS), zero external JS, ≥16 Cloudflare-resized hero
image URLs intact
## Why this matters
After merge, any push to `main` that adds, removes, or edits a skill,
design system, craft principle, or live-artifact template
automatically triggers a fresh build that:
1. picks up the new Markdown via the content-collection glob,
2. regenerates thumbnails for any matching example.html,
3. emits new sitemap entries and JSON-LD,
4. and ships to Cloudflare Pages — no landing-page-side change
required.
* fix(landing-page): address review feedback on PR #1158
Five fixes from the review pass — none change scope, all close the
"contradictory totals" / "stale data" / "silent CI failure" gaps the
reviewers flagged.
## Hero / catalog claims now read live counts everywhere
`apps/landing-page/app/page.tsx` previously hardcoded `31` skills and
`72` systems in the hero copy and stat rings, while the nav and meta
description had already moved to `getCatalogCounts()`. After this PR
every visible "X skills / Y systems" claim — hero lead, hero stat
rings, capabilities cards body copy, labs section meta + filter pills,
selected-work fractions, the labs CTA, and the footer Library — reads
from a single `counts` prop. `Header` and `Page` now both require
`counts` (no optional fallback) so a future caller can never silently
publish stale numbers.
The labs-section filter pills also stop being decorative buttons:
they now link to the actual `/skills/mode/<slug>/` and `/skills/`
catalog routes the new multi-page architecture exposes.
## Craft README no longer publishes
`apps/landing-page/app/_lib/catalog.ts` filtered out `e.id !== 'README'`,
but Astro normalizes `craft/README.md`'s id to lowercase `readme`, so
the published site shipped `/craft/readme/` as a public craft principle
and the nav badge counted 12 instead of 11. Compare case-insensitively
(`e.id.toLowerCase() !== 'readme'`) so any future README casing is
also filtered out. Verified locally: `apps/landing-page/out/craft/`
now contains exactly 11 entries.
## Preview URL preserves actual file extension
`listPreviews()` was already discovering `.png`, `.webp`, `.jpg`, and
`.jpeg`, but `previewUrlFor()` always emitted `.png`, so a future
sharp/webp post-processor (or a manually committed template asset)
would mark the record as available while the rendered `<img src>`
404'd. Switched the structure from `Set<slug>` to `Map<slug, filename>`
and emit the actual on-disk filename verbatim.
## Preview script: per-artifact soft, systemic hard
Previously any single failed `example.html` capture exited the script
non-zero, which forced both workflows to mark the entire preview step
`continue-on-error: true`. That blanket tolerance also masked
systemic generator failures — a chromium launch that never finds the
browser binary would silently ship a deploy with zero thumbnails.
`scripts/generate-previews.ts` now distinguishes:
- per-artifact failures → logged and skipped, exit 0 (catalog
degrades gracefully for those skills),
- discoverJobs / chromium.launch / 100%-failure run → exit 1
(systemic, must fail the build).
Both workflows drop their `continue-on-error: true` flags so a real
problem actually surfaces.
## AGENTS.md reflects the multi-page architecture
`apps/landing-page/AGENTS.md` previously declared the landing page
single-route ("Not multi-page. There is exactly one route ('/')").
That guidance is now wrong — there are six top-level route groups
(`/`, `/skills/`, `/systems/`, `/craft/`, `/templates/`, plus their
facet variants). Updated to describe content-collection sourcing, the
no-mirror rule, the auto-deploy workflow contract, and the
"never hardcode catalog claims" boundary.
## Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors,
0 warnings, 0 hints across 25 Astro files
- `pnpm --filter @open-design/landing-page build` — 340 pages built
(was 341 before the README filter; the README route is now
correctly absent), live counts visible in the built `out/index.html`:
`driven by 125 composable skills and 149 brand-grade design systems`
- Verified `out/craft/` no longer contains `readme/`
- Verified preview URLs resolve to the actual on-disk filename via
the regenerated catalog index page
* fix(landing-page): clean up live-artifact template name + summary parsing
Address @mrcfps's follow-up review on `0715d8c`. The
`shapeLiveArtifactTemplate()` parser was passing the README's H1
verbatim (literal backticks intact) and using the first non-empty
post-H1 line as the summary, even when that line was the
`> Category: **Live Artifacts**` editorial blockquote. Result:
`/templates/live-otd-operations-brief/` was shipping a
`<meta name="description" content=">">` and a card title with raw
Markdown noise — a regression for both SEO snippets and the
templates catalog at-a-glance scan.
## Two new shared helpers
- `stripMarkdownInline()` — strip backticks, asterisks, and link
wrappers so `# \`otd-operations-brief\` · live-artifact template`
becomes `otd-operations-brief · live-artifact template` before any
further trimming.
- `extractFirstProseParagraph()` — walk the body after the H1 and
skip blockquotes (`>`), list markers, table rows, fenced code, and
HR rules. Stop at the first contiguous prose paragraph and pass it
through `stripMarkdownInline()` so the result is human-readable.
Both helpers live next to `titleizeSlug()` and are used by
`shapeCraft()` and `shapeLiveArtifactTemplate()` so they share one
implementation.
## Live-artifact title boilerplate trim
Live-artifact READMEs commonly title themselves
`# \`<slug>\` · live-artifact template`. After stripping the inline
backticks the trailing `· live-artifact template` is redundant
("Templates" already groups them) and adds a wide noisy suffix on
catalog cards. Removed it via a narrow regex tail-strip.
## Result on the existing fixture
Verified locally for `templates/live-artifacts/otd-operations-brief/`:
- before: `<title>\`otd-operations-brief\` · live-artifact template …</title>`,
`<meta name="description" content=">">`
- after: `<title>otd-operations-brief — Open Design template</title>`,
`<meta name="description" content="A drop-in html_template_v1
live-artifact template for an editorial On-Time Delivery brief.
It ships:">`
Typecheck 0/0/0, build 340 pages.
---------
Co-authored-by: Joey <joey@cursor.so>
Co-authored-by: Joey-nexu <236967869+joeylee12629-star@users.noreply.github.com>
|
||
|
|
03da01a56f
|
ci: use open-design bot for contributors wall refresh (#1349) | ||
|
|
a4649dacb3
|
fix: check contributor tiers on review and comment events (#1248)
* fix: check contributor tiers on review and comment events Expand the contributor card workflow to run tier checks for PR reviews, issue comments, PR review comments, and discussion activity. The bot now understands pull_request_target directly, so remove the event-name shim. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: drop fork-unsafe triggers (review, issue_comment, review_comment) Per @mrcfps and @chatgpt-codex-connector review: GitHub withholds repository secrets on pull_request_review, pull_request_review_comment, and issue_comment events when they originate on forked PRs, so wiring those events here would fail-closed exactly for external contributors. Keep the fork-safe triggers (pull_request_target.closed, issues.opened, discussion.*, discussion_comment.*) and document why the three are excluded. They can be re-added later via a workflow_run handoff. --------- Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: qiongyu1999 <qiongyu1999@gmail.com> |
||
|
|
43f7fc536a
|
Add Langfuse telemetry relay (#1296)
* Add Langfuse telemetry relay * Configure telemetry worker custom domain * Add telemetry relay health check * Harden telemetry relay config |