release: Open Design 0.8.0 β Everything is a plugin. Headless. Plugins create plugins.#2461
Draft
lefarcen wants to merge 45 commits into
Draft
release: Open Design 0.8.0 β Everything is a plugin. Headless. Plugins create plugins.#2461lefarcen wants to merge 45 commits into
lefarcen wants to merge 45 commits into
Conversation
β¦ens (#2413) The desktop-pet companion window calls setVisibleOnAllWorkspaces so it floats across every Space. By default Electron's macOS implementation transforms the whole process type between UIElementApplication and ForegroundApplication while applying that call. On Electron 41 / macOS 26 that round-trip races during the launch burst β the pet window is created alongside the main window β and the process can stay stuck as an accessory app: no Dock icon, no menu bar, even though the windows render fine. Pass skipTransformProcessType: true so the pet window's all-Spaces behavior never touches the app's process type. The desktop pet is a cosmetic companion window; the main window keeps the app a regular Dock app. The pet still floats on every Space via its alwaysOnTop floating level. Fixes #2394
[codex] Refresh Open Design app visuals
* Fix Windows nightly release smoke identity * Fix mac nightly release smoke identity
Conflicts resolved by taking origin/main on all six points:
- apps/web/src/components/HomeHero.tsx:479-487 brand div removed
(main dropped the .home-hero__brand wrapper; the release-side visual
refresh still had it).
- apps/web/src/components/HomeHero.tsx:894-898 attach Icon size
18 (main's update) replaces 20 from release.
- apps/web/src/components/HomeHero.tsx:913-927 submit button uses
<Icon name="arrow-up" size={22} /> (main's component refactor)
instead of the release-side inline SVG.
- apps/web/src/components/EntryShell.tsx:578-582 Discord Icon size
14 (main) instead of 16 (release).
- apps/web/src/styles/home/home-hero.css drop .home-hero__brand /
__brand-mark / __brand-name rules β main removed both the component
div and these CSS rules together; keeping the CSS would be dead code.
- apps/web/src/styles/home/entry-layout.css Discord badge icon color
#5865f2 (main, the brand color introduced by PR #2386) instead of
release's neutral var(--text-strong).
Bumps 14 workspace package.json files from 0.7.0 to 0.8.0:
- root, apps/{web,daemon,desktop,landing-page}
- packages/{contracts,host,platform,sidecar,sidecar-proto}
- tools/{dev,pack,pr}, e2e
apps/packaged was already at 0.8.0 from the preview lane.
Independently versioned packages keep their own tracks.
Adds CHANGELOG [0.8.0] - 2026-05-20 entry covering the
305 PRs merged since 0.7.0 by 75 contributors:
- Plugin engine rebuild + Plugin Registry surface
- Headless by default (desktop is thin wrapper around CLI)
- Critique Theater Phases 9 through 16
- 149 design systems with structured tokens.css
- Italian locale + CJK font fallback
- Leonardo.ai, ElevenLabs, SenseAudio providers
- Windows packaged auto-update
- Visual refresh + Quick-brief discovery overhaul
- PostHog v2 analytics
- Manual edit UX overhaul
* Fix Windows silent reinstall detection * Remove duplicate entry plugins nav
Conflicts resolved by taking origin/main on both files. Root cause: main's PR #2460 (fix(landing): align logo.webp with brand icon) changed HomeHero.tsx's .home-hero__brand-mark to render <img src=/app-icon.svg> instead of an inlined <HeroBrandIcon /> SVG, and bundled the matching CSS (26px round badge with bg-panel + border + padding 2px) plus a gap/font-size tune. The release-side visual-refresh CSS still targeted the SVG layout (38px square, transparent, inset SVG selector). Keeping release's CSS would leave main's <img> unstyled. - apps/web/src/styles/home/home-hero.css three blocks, all taken from main: .home-hero__brand gap 8px, .home-hero__brand-mark redesigned for <img> child, .home-hero__brand-name font-size 16px. - apps/web/src/index.css two blocks, both taken from main: workspace tab close column 22px and .workspace-tab__close 18x18 (paired tune-down of tab UI spacing).
Conflict resolved by taking origin/main: - apps/web/src/components/EntryNavRail.tsx design-systems rail button icon name palette-filled (release-side) -> blocks (main); main's icon swap is part of the more recent design-systems rail pass.
β¦ds (#2508) * i18n: add translations for media provider coming soon section (#2415) * i18n: add translations for media provider coming soon section - Add 'settings.mediaProviderComingSoonHint' key to all 19 locales - Replace hardcoded English strings in SettingsDialog.tsx with i18n keys - Reuse existing 'tasks.comingSoon' and 'settings.agentInstall.docs' keys - Resolves TODO(i18n) comment at line 5091 * fix: escape single quotes in translation strings * fix: escape all single quotes in English translation string * feat(release): upload browser sourcemaps to PostHog for packaged builds Next.js was emitting minified JS with no browser sourcemaps, so PostHog Error Tracking surfaces frames like fO / fz / s4 / tD instead of real file:line locations. This wires up the full pipeline: - apps/web/next.config.ts: enable productionBrowserSourceMaps so next build emits .js.map alongside each chunk. - tools/pack/src/web-sourcemaps.ts: new helper that runs after next build and before any packaging step copies the web output into the Electron resources. Uses @posthog/cli to inject chunk IDs and upload sourcemaps to PostHog, then ALWAYS strips every .map under .next/static so source never ships inside an installer (saves ~14 MB per packaged image too). - tools/pack/src/{mac/workspace,win/app,linux}.ts: call processWebSourcemaps immediately after the @open-design/web build step. - tools/pack/src/config.ts: read POSTHOG_CLI_API_KEY + POSTHOG_CLI_PROJECT_ID (with POSTHOG_PERSONAL_API_KEY / POSTHOG_PROJECT_ID aliases) and expose them on ToolPackConfig with the same shape as the existing posthogKey / posthogHost fields. - .github/workflows/release-{beta,preview,stable}.yml: pass the new secrets through so all three release channels symbolicate stacks. When the API key is missing (PR builds, forks, local contributor builds), the helper logs and skips the upload β but still strips .map files. The strip step is unconditional because shipping a sourcemap is equivalent to shipping the source. Adds tools/pack/tests/web-sourcemaps.test.ts covering: missing chunks dir silently noop, no-map noop, strip-only path when credentials are absent, recursive walker for nested subdirectories. CLI happy path is left to the release workflow itself. Required follow-up (cannot push from code): add a repo secret named POSTHOG_CLI_API_KEY (the phx_ personal API key) and a repo var named POSTHOG_CLI_PROJECT_ID (the numeric project id, 420348 for our project) in nexu-io/open-design settings before merging. * fix(web-sourcemaps): use management host for CLI, not ingest host POSTHOG_HOST is the ingest URL (us.i.posthog.com) used by the runtime SDK to POST events to /capture/. The @posthog/cli sourcemap upload talks to the **management** API (us.posthog.com) and gets a 404 on the ingest host. The two are not interchangeable. Adds a separate `posthogCliHost` field on ToolPackConfig sourced from POSTHOG_CLI_HOST (with no fallback to POSTHOG_HOST). When the env is unset the @posthog/cli defaults to the US Cloud app host on its own, which is correct for our project β so this PR doesn't need a new repo variable for it. --------- Co-authored-by: Nicholas-Xiong <[email protected]>
β¦#2521) PostHog Error tracking was missing the vast majority of real exceptions: 1. posthog-js's capture_exceptions: true is silenced by opt_out_capturing, so every opted-out user vanished from the error feed even though we could perfectly safely keep collecting their stacks (the consent toggle's user copy gates analytics, not safety telemetry). 2. posthog-js is dynamically imported only after /api/analytics/config resolves AND the user has consented. Errors thrown during the first 1-2 seconds (React hydration, early effects) had no listener to catch them. Net effect: 14d $exception count was 54 events / 10 users across ~5k DAU, producing the misleading 99.93% crash-free curve in PostHog's dashboard. This PR makes exception capture independent of both gates: - apps/web/src/analytics/error-tracking.ts (new): own window.error + unhandledrejection handlers, in-memory buffer (capped at 50 entries), direct fetch to https://<host>/i/v0/e/ with the public phc_ key. Same scrub layer as the posthog-js path so file paths still get redacted. - apps/web/app/[[...slug]]/client-app.tsx: installErrorHandlers() at module-load, before React or any feature code can throw. - apps/web/src/analytics/provider.tsx: bootstrapExceptionTracking() in the identity useEffect, parallel to getAnalyticsClient() β runs regardless of consent state, fetches /api/analytics/config, hands the phc_ key + host + distinctId to the error tracker so buffered events can flush. - apps/web/src/analytics/client.ts: capture_exceptions: false so posthog-js stops also emitting $exception (would have produced duplicate events server-side); also re-bridges the error-tracking context inside the loaded() callback so future events inherit the fully-resolved appVersion / sessionId. - apps/daemon/src/server.ts + packages/contracts: /api/analytics/config now returns key + host even when consent=false. enabled still reflects only the analytics consent toggle (posthog-js full autocapture stays off when enabled=false), but the always-on error tracker can read key directly. Forks without POSTHOG_KEY still get key=null and the whole pipeline becomes a no-op β fork-safe by construction. - apps/web/src/analytics/scrub.ts: regex fix so packaged-mac paths like /Applications/Open Design.app/Contents/Resources/apps/web/... (which contain a space) get fully rewritten to app://apps/web/...; previously the [^\s] guard stopped at 'Open' and leaked the install dir. Validation: - pnpm --filter @open-design/web typecheck: pass - pnpm --filter @open-design/web test: 199 files / 1823 tests pass (includes 8 new error-tracking.test.ts cases for buffer cap, hook install, scrub, and direct dispatch) - pnpm --filter @open-design/daemon test: 250 files / 2971 tests pass - pnpm guard: pass After release/v0.8.0 ships and rolls out, expect the crash-free curve to drop from the artificial 99.93% to a realistic 95-98% β that's not a regression, it's the first time we're measuring it.
β¦cles (#2525) * fix(web): decouple privacy banner from onboarding and Settings lifecycles The first-run privacy banner used to be tightly bound to two unrelated surfaces: it was hidden whenever Settings was open, and the onboarding panel only navigated in after the user had resolved the banner. The coupling existed because the banner's z-index sat below modal backdrops, so showing both at once collided visually, and the banner+onboarding were linearized to avoid a "two unfinished things on screen" feel. This change makes the three surfaces independent: - Lift `.privacy-consent-banner` z-index above the modal-backdrop layer so the banner stays visible (and clickable) when Settings is open. The banner is already `pointer-events: none` with opt-in on its actionable children, so it does not steal clicks from the layer below. - Drop the `!settingsOpen` guard from `showPrivacyConsent`. - Drop the `privacyDecisionAt != null` guard from the bootstrap onboarding route; first-run users land on `/onboarding` purely based on `!onboardingCompleted`, and the banner sits on top in parallel. - Drop the `navigate(... onboarding)` side effect from the banner's `onAccept` β the banner only persists the privacy decision now. Bootstrap also had to be reshaped: the merged config is now computed outside the `setConfig` updater so navigation can happen synchronously after the state update. Calling `navigate` inside the updater triggered a React "setState while rendering" warning, and reading a captured flag after `setConfig` was unreliable because React 18+ batches the updater to the next render β the navigate condition was never observed. Existing test that asserted the old coupling ("banner unmounts while Settings is open") is inverted to lock in the new contract. * fix(web): defer privacy banner until onboarding is done and user lands on home Product feedback on the previous lifecycle change: the banner should not appear during the welcome panel. It should surface only: - immediately after the user Skips onboarding (lands directly on home), or - after the user finishes the design-system step and later returns to a home view from the project view they were dropped into. To capture both paths with a single rule, the banner now requires: 1. Daemon config hydrated (unchanged). 2. No privacy decision recorded yet (unchanged). 3. onboardingCompleted === true. 4. The current route is a home route (route.kind === 'home'). The Skip path already routes through finishOnboarding, which calls onCompleteOnboarding() + changeView('home') β that satisfies all four gates the moment Skip is clicked. The finish path (step 2: create design system) previously navigated to a project view without marking onboardingCompleted. This commit mirrors the Skip path by calling handleCompleteOnboarding() from the App-level renderDesignSystemCreation onCreated callback (the onboarding-specific use of DesignSystemCreationFlow). The shared DesignSystemFlow component is left untouched so the create-from-Settings entry point keeps its existing semantics. The route gate keeps the banner suppressed while the user is reading their just-created design system project. As soon as they navigate back to the entry shell (home route), the banner appears. Tests: - "withholds the privacy banner until onboarding completes" β covers gate 3 (onboardingCompleted=false while still on onboarding/home). - "withholds the privacy banner outside the home route" β covers gate 4 (user is on a project route, onboardingCompleted=true). - Existing "keeps the first-run privacy banner mounted while settings is open" still passes; the Settings/banner z-index relationship is independent of these gates. * fix(web): allow privacy banner to surface on non-home routes after onboarding Follow-up to the previous lifecycle change. After exercising the design- system finish path end-to-end, product wants the banner to appear in the project view the user is dropped into β the first generation is running in the background and the user is already waiting, so the disclosure can be acknowledged inline rather than being held back until they navigate back to a home view. The Skip path is unchanged: Skip routes the user to home and the banner appears there. This drops the `route.kind === 'home'` guard and the matching test, and adds a contract test that locks in banner visibility on a project route when `onboardingCompleted=true` and no privacy decision has been made.
β¦migration (#2527) * feat(observability): web lifecycle telemetry + stable installationId migration Two intertwined safety-telemetry additions for the 0.8.0 release. Web lifecycle observability --------------------------- New `apps/web/src/observability/` module installed at module load via client-app.tsx β alongside the existing error-tracking exception hooks from #2521. Reuses error-tracking's direct-fetch transport (the same consent-bypass + early-buffer guarantees) so every event flows even when the user has opted out of general analytics: - client_long_task PerformanceObserver longtask >100ms (real "feels janky" signal, FPS proxy) - client_white_screen app fails to mount after 5s; MutationObserver cancels the timer the moment the React root renders so a normal boot is zero events - client_resource_error capture-phase window.error catches failed <script>/<link>/<img>/<iframe> loads (chunk-load failures, broken artifact refs) - client_boot_timing navigationStart β load timings via Navigation Timing v2 - client_visibility_change visibilitychange + page lifetime - client_session_summary real foreground duration emitted on pagehide - client_run_stuck 5min watchdog on SSE runs that don't progress (#2464 / #2405 / #1451 in data form) - client_iframe_error FileViewer iframe load failures (iframe errors don't bubble to window, so the global resource-error observer can't see them) - desktop_renderer_crash Electron main observes render-process-gone and forwards to daemon /api/observability/event - daemon_uncaught_exception daemon_unhandled_rejection process-level handlers on the daemon error-tracking.ts is generalised: `reportSafetyEvent(name, props)` now exposes the same buffer + direct-fetch transport that `reportHandledException` used, with identical $exception wire shape preserved for the existing exception path. Daemon cross-process bridge --------------------------- New `AnalyticsService.captureSafety()` skips the consent re-check and posts via posthog-node with installationId as distinct_id. Wired into: - `POST /api/observability/event` for desktop main and any future helper process that needs to ship a safety event (no consent check β same contract as web's direct-fetch path) - `process.on('uncaughtException')` / `unhandledRejection` on the daemon itself Stable installationId across reinstalls (critical for 0.8.0 rollout) -------------------------------------------------------------------- installationId previously lived in `<namespace>/data/app-config.json`, so a packaged reinstall that churned the namespace token (or any future namespace-scoped data wipe) rotated the id and the user showed up as a brand-new PostHog person. This is the immediate trigger: when 0.8.0 ships, every 0.7.x user upgrading would silently double the user count. New module `apps/daemon/src/installation.ts` reads/writes `<installationDir>/installation.json` at the channel root. The daemon gets the path from `OD_INSTALLATION_DIR`, set by `apps/packaged/src/sidecars.ts` to `paths.installationRoot` (one level above `namespaces/` β e.g. `~/Library/Application Support/Open Design Nightly/` on mac). `readAppConfig` transparently merges: if installation.json has an id it wins; if only app-config.json has one (the 0.7.x state), it gets mirrored to installation.json on the next read. `writeAppConfig` mirrors any explicit installationId write, including the null-clear path used by Settings β "Delete my data". 7 call sites of readAppConfig keep their signatures unchanged. Survives: - same-channel reinstall (DMG drag-replace, NSIS reinstall) - namespace churn between packaged builds - per-namespace data reset (future installer that clears `<ns>/data/`) Still rotates (intentionally): - explicit "Delete my data" - manual `rm -rf "~/Library/Application Support/Open Design <Channel>/"` - different channel (Stable vs Nightly stay distinct because userData paths differ; that's the existing channel-isolation contract) What this changes for posthog-js -------------------------------- client.ts had `capture_exceptions: false` from #2521; nothing else changes. autocapture / $pageview / $autocapture / track() / daemon analyticsService.capture() β all unchanged. New events are additive. Validation ---------- - pnpm guard pass - pnpm typecheck whole repo pass - pnpm --filter @open-design/web test 200 files / 1824 tests - pnpm --filter @open-design/daemon test 251 files / 2981 tests (includes 10 new tests in installation.test.ts pinning the 0.7.x β 0.8.0 migration, namespace-wipe survival, delete-my-data clear, and fresh-id rotation) - pnpm --filter @open-design/packaged test 9 files / 89 tests - Pre-existing baseline: apps/desktop/src/main/updater.ts has typecheck references to RELEASE_CHANNEL_NAMES.PREVIEW/NIGHTLY on release/v0.8.0; unrelated to this PR. * fix(observability): preserve fatal exit on uncaught + skip loading shell in white-screen check Addresses codex review on PR #2527 (Siri-Ray). 1) Daemon process handlers must keep Node fatal semantics Installing an uncaughtException listener silences Node's default crash/exit; Node 15+ does the same for unhandledRejection when a listener is present. The previous handlers logged telemetry and let control return to the event loop, leaving a corrupted daemon serving requests instead of letting the supervisor restart it cleanly. triggerFatalShutdown() now: - dispatches captureSafety once (guarded against re-entry from cascading faults) - races posthog-node's shutdown against a 1s bounded timeout so a slow flush can't keep the process alive - calls process.exit(1) after the race resolves Both uncaughtException and unhandledRejection route through it. apps/daemon/tests/uncaught-fatal-shutdown.test.ts pins: - captureSafety is invoked exactly once even on repeated faults - exit(1) fires on the happy path - exit(1) still fires when shutdown hangs past the timeout - exit(1) still fires when captureSafety itself throws 2) White-screen detector treated the loading shell as a successful mount apps/web/app/[[...slug]]/client-app.tsx renders the dynamic-import fallback as <div class="od-loading-shell">Loading Open Designβ¦</div> whose visible text (19 chars) exceeded the previous 10-char floor. monitorMount() would therefore cancel the 5s timer the instant Next swapped the loading shell in, completely missing the white-screen signal the observer is meant to add. isAppMounted() now: - primary signal: <html data-od-app-mounted="1"> set by App.tsx's first useEffect β authoritative because once App has mounted at least once, any later tree crash is an $exception story, not a white-screen story - fallback: only counts children of the root container whose classList does NOT include known loading-shell markers (od-loading-shell). Their visible text drives the > MIN_VISIBLE_TEXT check, so the loading sentinel can never be mistaken for a mount. apps/web/tests/observability/white-screen.test.ts pins: - fires client_white_screen when only the loading shell is present after the timeout - does NOT fire when data-od-app-mounted is set before the timeout - cancels the timer the moment a real workspace-shell child appears alongside the loading shell - still fires when only sub-MIN_VISIBLE_TEXT non-shell content is present (effectively blank) Validation: - pnpm guard pass - pnpm typecheck pass - pnpm --filter @open-design/daemon test 252 files / 2985 tests - pnpm --filter @open-design/web test 201 files / 1828 tests * fix(observability): await captureSafety enqueue before fatal shutdown flush Addresses second-pass codex review on PR #2527 (Siri-Ray, 3279268246). The previous fatal-shutdown path called `analyticsService.captureSafety()` synchronously and immediately raced `analyticsService.shutdown()` against the bounded timeout. captureSafety in apps/daemon/src/analytics.ts does its real `client.capture()` call only inside an async IIFE after `await readInstallationIdSafe()` β so shutdown could win the race, drain an empty posthog-node queue, and let `process.exit(1)` run BEFORE the daemon crash event ever got enqueued. We'd then preserve the process-lifecycle contract but lose the exact signal this PR is adding. Changes: - AnalyticsService.captureSafety now returns Promise<void>. The async IIFE is gone; the body awaits readInstallationIdSafe directly so the returned promise resolves only AFTER client.capture() has been invoked (which is when posthog-node's local buffer contains the event). - server.ts triggerFatalShutdown awaits captureSafety, then calls shutdown, and races that whole sequence against the 1s bounded timeout. Capture failures still don't block exit (try/catch around the await). - NOOP_SERVICE.captureSafety becomes `async () => undefined` to match the new signature. - Fire-and-forget callers (/api/observability/event) are unaffected; voiding the returned promise keeps them non-blocking. apps/daemon/tests/uncaught-fatal-shutdown.test.ts adds the reviewer- requested fixture: - 'waits for the captureSafety promise to settle before invoking shutdown' β gives capture a 50ms delay and shutdown a separate 50ms delay so the intermediate "capture done / shutdown not yet" state is observable. - 'still aborts and exits if captureSafety hangs past the bounded timeout' β captureSafety never resolves; the outer 1s timeout still forces process.exit(1). Validation: - pnpm guard pass - pnpm typecheck whole repo pass - pnpm --filter @open-design/daemon test 252 files / 2987 tests
) (#2532) * fix(web): show feedback prompt on every successful assistant turn Previously the thumbs-up/down widget only appeared when the turn produced an `<artifact>`, wrote a file via Write/Edit, or emitted a live_artifact event. That left whole categories of completed turns β image/video generation via `generate_image` / `generate_video`, MCP tool runs, plain text answers β without any way for the user to rate them. Drop the `hasArtifactWork` gate from `isFeedbackEligible` and remove the helper functions that fed it. The remaining filters (streaming in progress, empty response, unfinished todos, runStatus !== "succeeded") still suppress the widget for genuinely incomplete turns. The PostHog `has_produced_files` field is still emitted so the analytics side can keep slicing feedback by whether the turn produced artifacts. * test(web): flip artifact-only feedback assertion in AssistantMessage.test.tsx Companion test file to `chat-feedback.test.tsx` β the same artifact-only visibility rule was duplicated here and asserted the text-only success case stays hidden. Flip that case to assert the widget now appears, and update the file-level comment so the new rule reads cleanly. The streaming / failed-run / empty-response cases keep their existing "hidden" assertions; those are still excluded under the new rule.
* fix(web): preserve automation ingest select chevron * fix(web): isolate automation ingest select chevron * fix(web): make dark select chevrons non-repeating
[codex] fix home example prompt presets
* fix: tighten packaged updater flow * test: prune noisy extended ui coverage * fix: hide unpublished release artifacts * test: validate release updater channels * fix: align prerelease release namespaces
β¦2544 into release/v0.8.0) (#2560) * feat(desktop): follow OS language in packaged builds Packaged Electron currently shows Open Design in en-US regardless of the OS language setting, because the renderer's i18n picks its locale from `navigator.language` and Chromium hard-codes that to en-US unless the host process intervenes. Browser users and `tools-dev` users are unaffected because their `navigator.language` already reflects the OS / browser preference. This change: - Adds `applyOsLocaleSwitch(app)` in `@open-design/desktop/main`. It reads `app.getPreferredSystemLanguages()[0]` and (when called before Electron's `ready` event) points Chromium's `--lang` flag at it, so the renderer's `navigator.language` follows the OS. Safe to call more than once: `appendSwitch` is a no-op once `app.isReady()`. - Calls the helper from both Electron entries: `apps/packaged` before its own `whenReady`, and `runDesktopMain` for tools-dev parity. - Forwards the resolved locale through `BrowserWindow.webPreferences.additionalArguments` as `--od-os-locale=<bcp-47>`, parsed by the preload and exposed at `window.__od__.client.osLocale`. The host bridge type (`OpenDesignHostClient.osLocale`) is extended accordingly. - Updates `detectInitialLocale` in `apps/web/src/i18n/index.tsx` to read that field as a new step between the existing localStorage and navigator fallbacks. Browser/web continues to fall through to `navigator.languages` unchanged. The explicit `osLocale` channel exists in addition to `--lang` because some `app.getPreferredSystemLanguages()` strings (e.g. `zh-Hant-TW`, `pt-PT`) need to round-trip through `resolveSystemLocale` to land on the right supported locale, which Chromium's `navigator.language` cannot do on its own. * fix(web): route OS locale read through getOpenDesignHost The first cut of detectInitialLocale read `window.__od__.client.osLocale` directly, which trips `tests/host-boundary.test.ts` β that guard test keeps web source from referencing preload globals by name so the boundary stays single-source. Switch to `getOpenDesignHost()` from `@open-design/host`, and rewrite the i18n test to install the host via `installMockOpenDesignHost` instead of poking the global directly. * fix(tools-pack): unblock mac packaged build on pnpm workspaces Two independent issues prevented `pnpm tools-pack mac build --to all` from completing on a clean macOS workspace, both unrelated to the desktop OS-locale change in this PR but bundled here because verifying that change end-to-end required the packaged pipeline to actually finish. 1. `apps/web/.next/standalone/node_modules/.pnpm/node_modules/<pkg>` contained dangling symlinks left by Next's nft trace (e.g. a `semver -> ../[email protected]/node_modules/semver` link to a `.pnpm/<pkg>@<ver>` directory pnpm never created). The downstream `cp { dereference: true }` aborted the whole packaged pipeline with ENOENT. Walk every artifact tree before copy and unlink symlinks whose target doesn't resolve. Targets that *do* resolve stay untouched. 2. Next 16's standalone build under pnpm workspaces does not hoist peer-dep packages (react, react-dom, styled-jsx) into `<standalone>/apps/web/node_modules`. The downstream `web-standalone-after-pack.cjs` audit then does `createRequire(server.js).resolve('react/package.json')`, whose module walk falls out of the standalone tree and aborts the electron-builder phase. Add a `hoistStandaloneNextPeerDeps` step for the web standalone artifact only: it locates the `<pkg>@<version>` (not peer-resolved sibling) directory under `.pnpm` and symlinks it into `apps/web/node_modules/<pkg>`. The subsequent `cp { dereference: true }` then writes the real directory into the cache so the packaged tree stays self-contained. Verified by `pnpm tools-pack mac build --to all` succeeding end-to-end (zip + dmg + app), then `pnpm tools-pack mac install` and `pnpm exec tools-pack mac inspect --expr` reading the desired `__od__.client.osLocale` from the packaged renderer. * feat(desktop): fold encodeURIComponent + manual locale source + pet window from #2554 Three defensive improvements lifted from @Eli-tangerine's parallel implementation on #2554, kept consistent with the OS-locale chain already on this branch: - The argv value crossing main β preload is now wrapped with encodeURIComponent / decodeURIComponent so a locale string with `;`, `=`, or any other Chromium argv special char round-trips cleanly. BCP-47 region tags don't carry those today, but the renderer parser no longer has to assume it. - `setLocale` now also writes `open-design:locale-source = "manual"` to localStorage, and `detectInitialLocale` only treats the stored locale as winning when that marker is present. An untagged value (left over from a future auto-write path, or a stale install) no longer pins the app to an old language once the host injects a fresh OS locale. Today `setLocale` is the only writer so the marker has no behaviour difference yet β this is a defensive net. - `createDesktopPetWindow` now receives `osLocale` and forwards the same `additionalArguments` as the main `BrowserWindow`, so the pet renderer's `__od__.client.osLocale` is consistent with the main window's instead of being silently undefined. Co-authored idea credit: changes mirror the locale-piece of @Eli-tangerine on #2554 β that PR is closing in favour of this one. Tests: detect-initial-locale gets a new "untagged localStorage value loses to host locale" case. desktop 62/62, host 13/13, web i18n + host-boundary 15/15 stay green. * feat(web): fold onboarding view styles from #2554 Pulls the 747-line addition to `apps/web/src/styles/home/entry-layout.css` from @Eli-tangerine's #2554 β the visual layer for the global onboarding flow (`/onboarding` view, Connect / About-you / Design-system steps). The view itself was already plumbed through `EntryShell.tsx`; this adds the styling that makes it shippable on v0.8.0. #2554 is closing in favour of this branch, so the CSS lands here so the onboarding work doesn't get dropped on the floor. Co-authored idea credit: @Eli-tangerine β original styling on #2554. * fix(tools-pack): make hoistStandaloneNextPeerDeps idempotent across builds Addresses non-blocking review by @PerishCode on #2560: the previous `if (await pathExists(linkPath)) continue;` guard uses `access()`, which follows symlinks. A stale symlink from a previous build whose `.pnpm/<pkg>@<version>` target moved (e.g. after a react/react-dom version bump that invalidates the workspace-build cache key and forces a re-run) reports as missing through `pathExists`, then `symlink()` rejects with EEXIST and the unhandled rejection aborts the packaged build. Switch to `lstat` (which does not follow the link) so we can tell "genuinely empty slot", "real directory left by Next" and "stale symlink" apart, then unlink stale entries before re-creating. Also move `stripBrokenSymlinks` ahead of `hoistStandaloneNextPeerDeps` in `copyWorkspaceBuildArtifactsToCache` so any leftover dangling links that survived a previous run are cleared before hoist tries to write.
#2459) * feat(analytics): emit file_upload_result from all three upload entries `file_upload_result` was wired only on the Design Files Upload button in FileWorkspace. The chat composer paperclip (project page) and the home hero composer paperclip uploaded files silently β PostHog dashboards saw upload activity from one of three real entry points, so per-surface funnels were invisible and totals undercounted. Three problems are fixed together: 1. `FileUploadResultProps` hard-coded `page_name: 'file_manager' / area: 'file_manager'`, which prevented the other two surfaces from type-checking. Widened to a discriminated union over the three v2 doc surfaces (`file_manager` / `chat_panel` chat_composer / `home` chat_composer). 2. `HomeChatComposerClickProps.element` was missing `'attachment'`, so the home composer paperclip had no usable click value even if we wanted to instrument it. Added the literal, mirroring the chat_panel composer. 3. Three call sites for `file_upload_result` would duplicate the per-file mime + total-bytes cohort math. Extracted to `apps/web/src/analytics/upload-tracking.ts#deriveUploadCohort` so FileWorkspace, ChatComposer, and the App.tsx Home submit path all compute the same `file_count` / `file_type` / `file_size_bucket` triplet. FileWorkspace's inline math is replaced with the shared helper to prevent drift. Call-site wiring: - HomeHero attach button: `ui_click` (`element='attachment'`) at click time. The actual upload is deferred to submit, so the `file_upload_result` for this surface fires from App.tsx after `uploadProjectFiles` resolves. - ChatComposer.uploadFiles: `file_upload_result` on success / failed / throw branches; existing `ui_click` (`element='attachment'`) at the paperclip stays as-is. - FileWorkspace.uploadFiles: refactored to use `deriveUploadCohort`; behavior unchanged. * test(analytics): cover deriveUploadCohort matrix Reviewer flagged that deriveUploadCohort silently fans out to three upload entry points (file_manager / chat_panel / home) but has no focused coverage, so a regression in zip detection, mixed-type collapsing, or the 1/10/100 MB thresholds would skew analytics without breaking any visible UI behavior. Adds homogeneous-image, zip-by-mime, zip-by-extension, mixed-type, empty-batch, bucket-boundary (1/10/100 MB), and defensive empty-mime cases.
Co-authored-by: qiongyu1999 <[email protected]>
* fix: tighten packaged updater flow * test: prune noisy extended ui coverage * fix: hide unpublished release artifacts * test: validate release updater channels * fix: align prerelease release namespaces * fix: align packaged updater validation
Co-authored-by: qiongyu1999 <[email protected]>
* Fix plugin publish and PR workflow UX * Update plugin workflow test expectations * Fix fake gh repo view verification path * Fix plugin publish headless tests and preserve PATH in shell wrappers. The publish-repo flow needs real git commits and fake gh auth output that matches gh auth status parsing. Login shells no longer drop PATH so test fakes and agent wrappers stay visible to nested gh/git calls. Co-authored-by: Cursor <[email protected]> * Restore plugin action card when share-task startup fails. If startGeneratedPluginShareTask rejects before a task is created, clear hiddenAssistantPluginActionPaths so the assistant action card reappears. Co-authored-by: Cursor <[email protected]> * Make daemon vitest self-contained for publish-github CLI shell-outs. Build dist/cli.js in tests/setup.ts when missing and set OD_DAEMON_CLI_PATH before server.ts resolves OD_BIN, so headless plugin tests pass from a clean checkout without a prior manual daemon build. Co-authored-by: Cursor <[email protected]> --------- Co-authored-by: Cursor <[email protected]>
β¦dback (#1558) * feat(analytics): PostHog + Langfuse instrumentation for assistant feedback Re-bases the original three-commit PR onto release/v0.8.0. The web-side feedback UI instrumentation (surface_view / ui_click / feedback_submit_result) landed on main while this branch was open, so on this rebase that wiring is taken from main; the remaining net additions are: - Contracts: TrackingFeedback* enums and the four dedicated assistant_feedback_* event payload types (click, reason_view, reason_click, reason_submit), plus normalizeCustomReason helper. The new event-name variants are added to TrackingEventName and the AnalyticsEventPayload discriminated union next to the existing surface_view/ui_click variants β both wire formats coexist. - POST /api/runs/:id/feedback in apps/daemon/src/chat-routes.ts: thin route that validates rating, allowlists reasonCodes through a simple string filter, and fire-and-forgets into the daemon's reportFeedback hook. - apps/daemon/src/langfuse-bridge.ts reportRunFeedbackFromDaemon forwards the rating + reasonCodes into Langfuse as user_rating (NUMERIC Β±1) + user_rating_reason (CATEGORICAL, one per code) score-create entries. Gates on telemetry.metrics + telemetry.content. - apps/web/src/providers/daemon.ts reportChatRunFeedback (fire-and-forget fetch) and apps/web/src/components/ProjectView.tsx wiring so each thumbs-up/down + reason submission posts the side-channel. Conflicts resolved (release/v0.8.0 vs the branch's old base): - packages/contracts/src/analytics/events.ts: keep main's file_upload_result / feedback_submit_result / settings_* event variants alongside the new assistant_feedback_* additions. - apps/daemon/src/server.ts: keep DNS-aware validateExternalApiBaseUrl, add reportFeedback closure wired into registerChatRoutes telemetry. - apps/daemon/src/chat-routes.ts: keep both /tool-result and the new /feedback routes; merge RegisterChatRoutesDeps to include both 'paths' and 'telemetry'. Drop PR's chat-routes-local reconcileAssistantMessageOnRunEnd helper (main has the equivalent in server.ts). - apps/web/src/components/ChatPane.tsx & AssistantMessage.tsx & ProjectView.tsx: keep main's projectKindForTracking prop name and its existing emission of surface_view / ui_click / feedback_submit_result; the PR's analyticsCtx-based reason_view/click/submit emission is dropped in this rebase since it would duplicate the existing wire format. - apps/web/tests/components/*: rename projectKind β projectKindForTracking to match ChatPane's current prop name. Outstanding review feedback (from the pre-rebase round, will be addressed in a follow-up commit): - AssistantMessage tests not yet passing the new feedback context to the direct render path. - ProjectView clear-feedback path skips reportChatRunFeedback, leaving stale Langfuse user_rating scores. - buildFeedbackPayload has no deletion path for previously-submitted user_rating_reason scores when the user switches thumbs. - POST /api/runs/:id/feedback always returns {status:'accepted'} even when consent is off; needs to surface skipped_consent / skipped_no_sink. - reasonCodes are filtered to string[] but not allowlisted against ChatMessageFeedbackReasonCode or deduped. * fix(analytics): address review on assistant feedback rebase Picks up the in-scope correctness items from the prior review round and the rebase residue without rewriting history: - chat-routes.ts: `/feedback` now awaits the daemon's preflight outcome and echoes it as the response. The contract was already shaped as `accepted | skipped_consent | skipped_no_sink`, but the previous handler always returned `accepted` because the network send was fire-and-forget. The consent + sink decision is local (a small file read and an env-var lookup); the actual Langfuse upload still runs as a detached promise. - chat-routes.ts: reasonCodes are now allowlisted against the contract's reason-code union and deduplicated before reaching Langfuse, so a stale or replayed client can't poison the Langfuse score table with unknown categorical values or duplicate stable ids in the same batch. - langfuse-bridge.ts: split the consent + sink resolution from the fire-and-forget network send so the route can claim `accepted` honestly. The legacy `skipped_no_sink` return on app-config read failure is preserved. Contracts + comment hygiene: - TrackingFeedbackReasonCode in packages/contracts/src/analytics/events.ts drifted from ChatMessageFeedbackReasonCode in packages/contracts/src/api/chat.ts; add `followed_design_system` and `missed_design_system` so the analytics wire format stays aligned with the persistence shape. - langfuse-trace.ts buildFeedbackPayload: the docblock claimed the raw custom-reason text is bucketed before send. Product reversed that on 2026-05-13 (raw text now ships, consent-gated). Replace the stale comment with the real semantics + a note that there is no tombstone path for reason codes the user removes in a follow-up submission (left as scope for a later PR). - AssistantMessage.tsx: remove the now-unused `AssistantFeedbackAnalyticsCtx` interface and a stray blank-line delete from the rebase; restore the analytics-context comment above the feedback hook. Left as follow-up (intentional, documented in code): - Sending a tombstone score when the user clears their rating β ProjectView still skips reportChatRunFeedback on `change===null`, so Langfuse retains the previous rating until the user re-submits. The PostHog event captures the clear separately. - Removing reason-code scores when the user re-submits with a smaller set β buildFeedbackPayload only overwrites the codes present in the current payload. * feat(analytics): wire PR's dedicated assistant_feedback_* events The four dedicated event types (`assistant_feedback_click` / `_reason_view` / `_reason_click` / `_reason_submit`) the PR added to contracts were sitting unused after the rebase because main's umbrella `surface_view` / `ui_click` / `feedback_submit_result` emissions covered the same user gestures. Wire the dedicated events alongside the umbrella ones so both wire formats fire on every feedback action β dashboards / evals can pick whichever schema they were built against without losing signal. Each dedicated event has stricter typing than its umbrella sibling (`project_id` / `project_kind` / `conversation_id` are non-null), so the new emissions are guarded behind a presence check and skipped on test renders that mount AssistantMessage without project context. The umbrella emissions retain their nullable fallbacks unchanged. Pairing: - surface_view (feedback reason panel) β assistant_feedback_reason_view - ui_click (feedback button) β assistant_feedback_click - ui_click (reason submit button) β assistant_feedback_reason_click - feedback_submit_result β assistant_feedback_reason_submit Reason click + submit share the existing `requestId` so PostHog can stitch clickβresult across both schemas, matching the spec.
* fix: tighten packaged updater flow * test: prune noisy extended ui coverage * fix: hide unpublished release artifacts * test: validate release updater channels * fix: align prerelease release namespaces * fix: align packaged updater validation * test: seed Windows release smoke config in app data
β¦ver surface_view (#2590) * feat(analytics): onboarding ui_click + lifecycle + update_popover surface_view Spec rows 1-3 of the Onboarding family (ui_click, onboarding_runtime_scan_result, onboarding_complete_result) and the home `update_popover` surface_view were all listed as P0 in the v2 doc but unwired β PostHog showed 0 events for every onboarding ui_click, 0 for the scan/complete result events, and 0 for the update-popover exposure. Contract (`packages/contracts/src/analytics/events.ts`): - Adds event names `onboarding_runtime_scan_result` / `onboarding_complete_result` and wires them into `AnalyticsEventPayload`. - Adds `OnboardingClickProps` (page_name=onboarding, area/element/ action discriminators + optional runtime/about_you/source rider fields) and threads it into `UiClickProps`. - Adds `OnboardingRuntimeScanResultProps` and `OnboardingCompleteResultProps` with the doc's full field set β enums for runtime_type / scan result / completion result / completion_type, plus the lifecycle context (has_about_you, has_design_system_request, source_count, exit_step_name). - Extends `TrackingFileUploadSurface` with an `onboarding / design_system_source` shape so the design-system-step source ingest can ride the same `file_upload_result` event the file_manager / chat composer already use. `source_type` is required on this shape so the dashboard can split by `local_code|fig|assets` without inspecting `file_type`. - Adds `UpdatePopoverSurfaceViewProps` for the home toolbar's "Update ready" panel. Onboarding wiring (`apps/web/src/components/EntryShell.tsx`): - Centralises step/runtime-context derivation in `emitOnboardingClick` + `emitOnboardingComplete` helpers; every interactive control inside OnboardingView now fires through one of them so a future spec tweak changes one place. - Click rows for runtime cards (local_coding_agent / byok), design- source cards (github_repo / local_code / fig_upload), about_you selects (organization_size / use_case / hear_about_us), and the Continue / Back / Skip navigation buttons. Multi-select use_case emits one row per added value, not per render. - `scanCliAgents` now emits `onboarding_runtime_scan_result` with detected/available counts on every terminal state β success when any CLI is available, failed when scan returned zero or threw. `duration_ms` measures wall-clock from start to terminal. - `onboarding_complete_result` fires from the Skip / last-step Continue / Generate paths with the right `completion_type`. The Generate path uses a new `DesignSystemCreationFlow.onBeforeGenerate` callback so the embedded flow can expose its local source-count state to the wrapper. DS creation flow (`apps/web/src/components/DesignSystemFlow.tsx`): - New `onBeforeGenerate(snapshot)` prop with a typed `DesignSystemGenerateSnapshot` shape. Fired right before the async generate() work; OnboardingView consumes it for both the `generate` ui_click (with source_type derived from which-counts-equal-total) and the completion lifecycle event. - `renderDesignSystemCreation` in `EntryView` / `EntryShell` / `App` grows a second `hooks` arg that plumbs `onBeforeGenerate` through. Update popover (`apps/web/src/components/UpdaterPopup.tsx`): - Fires `surface_view page_name=home area=update_popover` once per panel-open transition, deduped by `app_version_before -> app_version_after` so a re-render of the same offer doesn't inflate the count. Validation: - `pnpm guard` β - `pnpm --filter @open-design/web typecheck` β - `pnpm --filter @open-design/web test` β 203 files / 1828 tests - `pnpm --filter @open-design/daemon test` β 249 files / 2977 tests * fix(analytics): generation_progress fires from chat_panel + complete_result uses snapshot E2E (2026-05-21, distinct_id=e2e-onboarding-test-001) drove the full welcome flow and exposed two issues in the previous commit: 1. `page_view page_name=onboarding area=generation_progress` (step 4) never fired. PR #2590's commit wired this from `DesignSystemDetailView`, but the Generate path actually navigates to ProjectView (`page_name=chat_panel`), not to the DS detail surface. PostHog showed `chat_panel` and `file_manager` page_views landing right after the Generate click but no `area=generation_progress` row. Fix: fire `area=generation_progress` from `ProjectView` right alongside its `chat_panel` page_view when an onboarding session id is still in sessionStorage. Clear the session id immediately after so a later unrelated project visit doesn't inherit the onboarding attribution. The `DesignSystemDetailView` site can stay as a defense-in-depth β same dedup guard, no double-fire. 2. `onboarding_complete_result` from the Generate path shipped with `has_design_system_request: false` and `source_count: 0`. The `emitOnboardingComplete` helper read `designSource` (the click state on the three source-type cards), but E2E showed users click Generate without clicking those cards β they type a brand description and add a GitHub URL directly in the embedded form, so `designSource` stays null even when a request is clearly in flight. Fix: thread `DesignSystemGenerateSnapshot` from the `onBeforeGenerate` callback into `emitOnboardingComplete` via a new `extra.sourceSnapshot` option. When present, derive `has_design_system_request` from `sourceCount > 0 || hasBrandDescription` and `source_count` from the snapshot's `sourceCount`. Skip / last-step Continue paths still fall back to the `designSource` heuristic since no snapshot exists there. * fix(analytics): emit artifact_count from new-html count + remove unmount session-id clear Cherry-picked from the orphaned `fix/analytics-app-version-zero` HEAD (commit 5b5a7ed β pushed after PR #2453 had already squash-merged, never made it into release/v0.8.0). Two P0 data bugs: 1. `run_finished.artifact_count` was hard-coded `0` at `server.ts:11061` (now `:11394`). Every run on PostHog reported zero artifacts, breaking the "generation success β artifact produced" funnel. Fix: count incremental `.html` paths the run wrote or edited, deduped per path so a Write-then-Edit cycle on the same file counts as one artifact. Pure helper in `apps/daemon/src/run-artifacts.ts` with 10 unit tests covering empty / no-html runs, single Write, dedup across Write+Edit+ MultiEdit, distinct paths, Codex aliases (create_file, str_replace_edit), both `file_path` and `path` input shapes, case-insensitive extension, non-agent / malformed payloads, and Read/Grep/Bash always ignored. Wired into server.ts's `run_finished` properties block. 2. `OnboardingView` cleared `onboardingSessionId` on unmount. The Generate path unmounts OnboardingView *before* the post-Generate page_view fires elsewhere, so an unmount-clear consistently wiped the id before the 4th-step emission could read it. PostHog showed zero `area=generation_progress` events. Fix: drop the unmount cleanup effect entirely. Skip / Back / last-step Continue paths clear inline in their respective handlers (already in place from this PR's earlier instrumentation commit). The Generate path's clear now lives in `ProjectView` right after the `chat_panel` page_view (and the `generation_progress` page_view that rides with it). Abandoned sessions clear on sessionStorage tab close. * fix(analytics): emit onboarding complete after generate settles + text source_type Two review fixes on PR #2590 from mrcfps (2026-05-21 14:11): 1. `onboarding_complete_result` was emitted from `onBeforeGenerate`, which fires synchronously BEFORE `DesignSystemCreationFlow.generate()` runs the async draft-create / workspace-open work. Both of those have failure branches that bounce the user back to the setup form with an error. In that case the lifecycle row would have shipped as `result=completed` / `completion_type=completed_with_design_system` even though no design system was actually generated. Fix: add a new `onGenerateSettled(snapshot, outcome)` callback to `DesignSystemCreationFlow` and fire it from each branch of the `generate()` function (success after `onCreated` / failed on draft-create returning null / failed on workspace-open returning null / failed on catch). OnboardingView keeps the `onBeforeGenerate` hook for the intent-only `generate` ui_click row, and moves the lifecycle complete emit into `onGenerateSettled`. Failed outcomes ship as `result=failed` + `completion_type=completed_without_design_system` + the daemon's error code, and clear the onboarding session id since the user stays in the wrapper. 2. The `source_type` ternary in OnboardingView's `generate` ui_click mapped `sourceCount === 0` to `'none'` unconditionally, so a prompt-only generate ("user only typed a brand description, no GitHub / local / fig / assets sources") was indistinguishable on PostHog from "no input at all". The v2 contract reserves the `'text'` literal precisely for that prompt-only path. Fix: extract a `deriveOnboardingSourceType(snapshot)` helper that returns `'text'` when `sourceCount === 0 && hasBrandDescription`, `'none'` only when both are absent, single-source literal when one kind dominates, `'mixed'` otherwise. Single source of truth for the mapping so the ui_click and any future complete-row tagging stay consistent. * fix(analytics): countNewHtmlArtifacts skips failed tool ops Review fix on PR #2590 from mrcfps (2026-05-21 14:30, on commit 9e9a001). `countNewHtmlArtifacts` counted every `Write` / `Edit` tool_use on a `.html` path regardless of whether the matching `tool_result` came back with `isError: true`. A permission denied `Write index.html`, a path-outside-cwd refusal, or a parent-missing failure all still bumped `run_finished.artifact_count` to 1 β which is exactly the corruption pattern this helper was introduced to fix (hard-coded zero β spuriously > 0 is the same class of broken funnel signal). Fix: mirror the web-side `apps/web/src/runtime/file-ops.ts` pattern. Build a `resultByToolUseId` map in a first pass, then in the second pass only count a tool_use whose paired result exists AND `isError !== true`. A tool_use with no matching result is treated as "still in flight" and not counted; the dashboard would rather under-count attempts than promise artifacts we can't confirm landed. Tests grow 3 β 13: - successful Write pair counts (canonical path) - isError=true result does NOT count - unpaired tool_use does NOT count - Write-success-then-Edit-fail on same path still counts (artifact is on disk; later edit failure doesn't unmake it) - existing dedup / distinct-paths / alias / case / malformed / read-skip cases all updated to use the new pair() helper * fix(analytics): re-arm onboarding lifecycle on generate failure for retry Review fix on PR #2590 from mrcfps (2026-05-21 14:45, on commit 2cd05f0). The previous `onGenerateSettled` failure branch did two things that together broke the retry path: 1. Flipped `lifecycleReportedRef.current` to `true` (via `emitOnboardingComplete`), which the same guard then uses to short-circuit every subsequent complete emit. 2. Called `clearOnboardingSessionId()`, wiping the sessionStorage id that downstream surfaces (ProjectView's `generation_progress` page_view, subsequent ui_click rows) need to attribute under the same funnel session. But `DesignSystemCreationFlow.generate()` doesn't bail out on failure β it `setStep('setup')` and leaves the user in the same embedded form to try again. So the retry sequence used to look like: click Generate β fails β complete(failed) β flag locked + id cleared user fixes input β click Generate again ui_click `generate` row β fires under the STALE in-memory ref (sessionStorage was cleared but `onboardingSessionIdRef.current` still holds the old uuid) generate succeeds β onGenerateSettled(success) β emitOnboardingComplete β lifecycleReportedRef guard returns early β second complete row never lands navigate to ProjectView β peekOnboardingSessionId() = null β step-4 `area=generation_progress` row never lands Fix: the failure handler keeps the session id intact and just re-arms `lifecycleReportedRef.current = false`. A retry then emits a fresh complete row under the same `onboarding_session_id` (useful for "N retries until success" analysis) and an eventual success can still hand off through ProjectView with the id available for the step-4 emission. The Skip / last-step Continue paths still clear via the inline `clearOnboardingSessionId()` next to their `onFinish()` because those terminate the flow explicitly.
Co-authored-by: qiongyu1999 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
β
305 PRsΒ·75 contributorsΒ· 7 days β This isn't a release. It's a starting gun. We rebuilt Open Design's core. Then we ported the old world into it.Three things are different underneath now:
Two movements behind the rebuild:
This is the first release where both movements are real in the same engine. Full announcement and discussion thread β
π₯ Highlights
@bulai0408,@YUHAO-corn,@xxiaoxiong, and everyone who shaped the new shape.@fuyizheng3120.@lefarcen,@leessju.@bulai0408,@zoeforfun,@mturac./api/critique/conformanceβ dark-launched M0 by default. (feat(web): Critique Theater Phase 9 (drop-in mount wrapper, native i18n for de, ja, ko, zh-TW)Β #1315βfeat: Critique Theater Phase 15 (rollout resolver + Settings toggle hook)Β #1320, feat: Critique Theater wireup (activate the stack, M0 dark-launch by default)Β #1338, test(e2e): Critique Theater Phase 11 activation (un-fixme suite, seeded-project nav, split SSE fixture)Β #1483βfeat(daemon): Critique Theater Phase 12 (9 Prometheus metrics + 6 log events + OTel span + Grafana dashboard)Β #1485, feat(daemon): Critique Theater Phase 16 (M-phase rollout ratchet + /api/critique/conformance)Β #1499) Thanks@Nagendhra-web.tokens.css+ components manifest. Brand-token fixtures landed for 60+ new brands (Apple, Stripe, Airbnb, Vercel, Notion, Linear, GitHub, Figma, Slack, Discord, OpenAI, Shopify, Spotify, Uber, Cursor, and 50 more across AI / SaaS / devtool / fintech / docs / consumer / hardware / cultural categories) β each shipstokens.css+components.html, served through the design-system token channel that's now default-on. Plus design-system project import. (feat(daemon): make design-system token channel default-on (PR-D)Β #1544, feat(design-systems): add structured tokens for cursor, apple, stripe, airbnb, vercel brandsΒ #1652, feat(design-systems): add structured tokens for notion, linear, github, figma, slack, discord, openai, shopify, spotify, uberΒ #1794, feat(design-systems): add tokens.css + components.html for 9 AI product brandsΒ #1841, feat(design-systems): add tokens.css + components.html for 10 AI / devtool brandsΒ #2023, feat(design-systems): add tokens.css + components.html for 10 SaaS / consumer brandsΒ #2028, feat(design-systems): add tokens.css + components.html for 10 devtool / fintech / docs brandsΒ #2029, feat(design-systems): add tokens.css + components.html for 10 consumer / hardware / cultural brandsΒ #2033, feat(design-systems): add next 20 brand token fixturesΒ #2037, feat(design-systems): add 20 product and style token fixturesΒ #2040, feat(design-systems): add remaining style token fixturesΒ #2043, chore(design-systems): report component fixture coverageΒ #2049, feat(design-systems): extract component manifestsΒ #2051) Thanks@zoeforfun.@xxiaoxiong,@bulai0408,@Fl0rencess720,@mzl163.@PerishCode,@mturac.@xxiaoxiong,@delamoer.@lefarcen,@YUHAO-corn,@bulai0408.@alchemistklk,@nombreregular.page_name+ onboarding/design-system page_views, modernized event taxonomy so product analytics finally reflects the new plugin world. (feat(analytics): ship PostHog v2 event schemaΒ #2285, feat(analytics): unify page_name + onboarding/design-system page_viewsΒ #2390) Thanks@lefarcen.@bankielewicz./html-anythingevangelism page (feat(landing-page): add HTML Anything page and responsive headerΒ #2452), rebuilt/templatescatalog backed bydesign-templates(feat(landing): rebuild /templates/ catalog from design-templatesΒ #2369), tutorials section (feat(landing): refresh templates and add tutorials channelΒ #2409). Thanks@Tuola-waj,@ashleyashli.β¨ What's New
π§© Plugin engine, registry & publishing
@bulai0408.@lefarcen.@leessju.@bulai0408.@bulai0408.@zoeforfun,@lefarcen.@bulai0408.@bulai0408.@mturac.π Critique Theater (Phases 9 β 16)
de/ja/ko/zh-TW. (feat(web): Critique Theater Phase 9 (drop-in mount wrapper, native i18n for de, ja, ko, zh-TW)Β #1315) Thanks@Nagendhra-web./api/critique/conformance. (feat(daemon): Critique Theater Phase 16 (M-phase rollout ratchet + /api/critique/conformance)Β #1499)π¨ Design systems & tokens
@zoeforfun.tokens.cssfor 60+ brands across AI, devtool, SaaS, fintech, docs, consumer, hardware, cultural categories β Apple, Stripe, Airbnb, Vercel, Notion, Linear, GitHub, Figma, Slack, Discord, OpenAI, Shopify, Spotify, Uber, Cursor, and many more. (feat(design-systems): add structured tokens for cursor, apple, stripe, airbnb, vercel brandsΒ #1652, feat(design-systems): add structured tokens for notion, linear, github, figma, slack, discord, openai, shopify, spotify, uberΒ #1794, feat(design-systems): add tokens.css + components.html for 9 AI product brandsΒ #1841, feat(design-systems): add tokens.css + components.html for 10 AI / devtool brandsΒ #2023, feat(design-systems): add tokens.css + components.html for 10 SaaS / consumer brandsΒ #2028, feat(design-systems): add tokens.css + components.html for 10 devtool / fintech / docs brandsΒ #2029, feat(design-systems): add tokens.css + components.html for 10 consumer / hardware / cultural brandsΒ #2033) Thanks@zoeforfun.@MJ-thunder.@zoeforfun.@leessju.π€ Agents, providers & media
@fuyizheng3120.@xxiaoxiong.@bulai0408.@Fl0rencess720,@mzl163.@bulai0408.@PerishCode.@Derrick-xn.@alchemistklk.@lefarcen.@alchemistklk.CODEX_API_KEYsupport for the Codex CLI lane. Thanks@alchemistklk.@mutoe.π₯οΈ Web UI
@bankielewicz.@nombreregular.@alchemistklk.@xxiaoxiong.@xxiaoxiong.@xxiaoxiong.@xxiaoxiong.@xxiaoxiong.@YUHAO-corn.@asim48-ctrl.@xxiaoxiong.@Nagendhra-web.@lefarcen.ππͺ Desktop & packaging
@PerishCode.@PerishCode,@mturac.@PerishCode.@PerishCode.@PerishCode.@PerishCode.@lefarcen.@PerishCode.@PerishCode.@jiannanya.π Internationalization
@xxiaoxiong.@delamoer.@YUHAO-corn.@YUHAO-corn.@YUHAO-corn.@YUHAO-corn.@YUHAO-corn.@YUHAO-corn.pnpm i18n:coverage). Thanks@YUHAO-corn.π Analytics, observability & infra
@lefarcen.page_name+ onboarding / design-system page_views. (feat(analytics): unify page_name + onboarding/design-system page_viewsΒ #2390) Thanks@lefarcen.posthog-node4 β 5 in the daemon. (chore(deps): upgrade posthog-node 4 -> 5 in daemonΒ #2309) Thanks@neogenix.@bulai0408.@lefarcen.π¦ Templates, landing & tutorials
@Tuola-waj./templatescatalog fromdesign-templates. (feat(landing): rebuild /templates/ catalog from design-templatesΒ #2369) Thanks@ashleyashli.@ashleyashli.@ashleyashli.@lefarcen.@lefarcen.@lefarcen.@bulai0408.Refresh-Templatesflow. Thanks@ashleyashli.π Selected fixes
Web
@bulai0408.@bulai0408.@YUHAO-corn.@Siri-Ray.@Siri-Ray.@zoeforfun.@YUHAO-corn.@YUHAO-corn.@bulai0408.@YUHAO-corn.@YUHAO-corn.@YUHAO-corn.@bulai0408.@xxiaoxiong.@PerishCode.@PerishCode.@xxiaoxiong.@bulai0408.@bulai0408.@PerishCode.@xxiaoxiong.Desktop & packaging
@PerishCode.@PerishCode.@PerishCode.@mturac.@PerishCode.@PerishCode.@PerishCode.@PerishCode.@PerishCode.Daemon, runtime & connectivity
@bulai0408.@YUHAO-corn.@alchemistklk.@lefarcen.@lefarcen.@shangxinyu1.@alchemistklk.@bulai0408.@PerishCode.π Documentation
@Nagendhra-web.@PerishCode.@YUHAO-corn.@YUHAO-corn.zh-TWREADME with the English version. Thanks@YUHAO-corn.@YUHAO-corn.@PerishCode.@leessju.@lefarcen.main. (docs: point 0.8.0 preview contributors at mainΒ #1846)π¨ For Developers
Click to expand
@Nagendhra-web.@Nagendhra-web.@lefarcen.@zoeforfun.@lefarcen.pnpm i18n:coverageinformational report. Thanks@YUHAO-corn.@lefarcen.@lefarcen.β System requirements
install/start/stopfrom CLI). Packaged GUI artifact is still deferred while the release lane is hardened.flake.nixwith home-manager and NixOS support.enginesinpackage.json).odCLI shadows POSIXodwhen installed globally. Use/usr/bin/odorcommand odfor the system tool.π Thanks to everyone who shipped 0.8.0
305 PRs by 75 contributors. The "everything is a plugin" thesis only works because so many of you came at it from so many different angles. Thank you to:
@Abepena,@Derrick-xn,@Fl0rencess720,@GHX5T-SOL,@Hetsavani,@Jeshua09090,@Lucky19112002,@MJ-thunder,@MetaAlms,@MrRockySL,@Nagendhra-web,@PerishCode,@Priyanshudotdev,@Romantin,@Sid-Qin,@Siri-Ray,@Tuola-waj,@YUHAO-corn,@ZZXX-bit,@aaronjmars,@abhid-007,@alchemistklk,@ashleyashli,@asim48-ctrl,@bankielewicz,@bluto447,@bulai0408,@davezfr,@delamoer,@digitalcr8tive,@enaktes9-hub,@epicsagas,@fancyboi999,@feliciaZH,@fuyizheng3120,@hahaplus,@heylakatos,@hobostay,@jeongjin0,@jiannanya,@laihenyi,@leessju,@lefarcen,@mar2181,@mrcfps,@mrzhangkris,@mturac,@mutoe,@mzl163,@mzl2233,@neogenix,@nettee,@ngoduybien,@nmsn,@nombreregular,@ojhendershot,@orbisai0security,@pftom,@portseif,@prantikmedhi,@quangdo126,@sakshyasinha,@samay-hash,@sasha1107,@shangxinyu1,@slamj1,@spurnout,@sukumarp2022,@toby-bridges,@whoughton,@xxiaoxiong,@yuhaoyuan,@zellux,@zhangdongming0607,@zoeforfun.A movement doesn't ship from one team's laptops; it ships from the people who showed up early and built the missing pieces. We see you. π«‘
π Releasing this PR
This PR ships:
package.jsonfiles at0.7.0bumped to0.8.0(root,apps/{web,daemon,desktop,landing-page},packages/{contracts,host,platform,sidecar,sidecar-proto},tools/{dev,pack,pr},e2e).apps/packagedwas already0.8.0from the preview lane. Independently versioned packages (packages/agui-adapter,packages/diagnostics,packages/plugin-runtime,packages/registry-protocol,apps/telemetry-worker,tools/serve) stay on their own tracks.CHANGELOG.mdβ new[0.8.0] - 2026-05-20entry that crystallises the 305 PRs into a digestible release history.release/v0.8.0.The standard release path is:
main(release-branch merge-back, as with 0.7.0).release-stableworkflow withchannel=nightlyto validate, thenchannel=stable, nightly_version=...to promote.release-stablecreates theopen-design-v0.8.0tag, builds + signs mac (arm64 + Intel) artefacts, builds Windows NSIS, and publishes the GitHub Release.Full Changelog: open-design-v0.7.0...release/v0.8.0