mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix/template-project-create-flow
1171
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c8358d4618 | Fix template project creation flow | ||
|
|
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 |
||
|
|
579db9e8c7 |
feat(analytics): unify page_name + onboarding/design-system page_views (#2390)
Doc v2 row 12 (settings) was renamed `page` -> `page_name` in the doc; mirror that in code so every event surface uses the same field name on the wire. Drops `TrackingSettingsPage` as a separate discriminator and merges 'settings' into `TrackingPageName` so `page_name='settings'` sits beside the product surfaces. Adds two new page_view surfaces from the v2 doc: - `page_name=onboarding`: one emission per step exposure in the Connect / About you / Design system / Generation 4-step funnel. `onboarding_session_id` is generated once in sessionStorage so the 4th step (which fires from `DesignSystemDetailView` after onboarding navigates away) stays attributable. Cleared on onboarding finish so a later unrelated DS visit doesn't inherit the id. - `page_name=design_system_project | design_systems`: multi-surface DS page_view. Wires the page-level emissions (list / create / generation / preview); the module / popover / panel exposures inside home and studio are intentionally left for a follow-up so this PR doesn't blur with the unresolved "page_view vs surface_view for non-page exposures" question raised with product. `PageViewProps` becomes a discriminated union (`GenericPageViewProps | OnboardingPageViewProps | DesignSystemsPageViewProps`) so each surface gets its own typed contract. `design_system_source` is a new field on the DS page_view; it shares the name from the v2 doc with the existing run_created/run_finished field, but the value set is disjoint so a new `TrackingDesignSystemOrigin` type carries it. |
||
|
|
c80acfefeb |
fix(daemon,web): block pitch-deck placeholder publishes and unbreak framework decks (#2384)
Two preview-time bugs surfaced ahead of 0.8.0: 1. Pitch-deck example (#2215): the official html-ppt-pitch-deck prompt asked the agent to confirm three facts first, but the manifest had no structured `od.inputs`, so the platform's required-input gate had no fields to enforce and the run could publish HTML that still contained unresolved fundraising placeholders (`Name to confirm`, `$X.XM`, `Replace this panel with`, ...). Add structured required inputs to the manifest and a daemon-side publication guard that rejects HTML/deck artifact writes whose body still contains those placeholders. Scope is the file-write boundary only (no assistant-text scanning), so the guard cannot trip on the agent's chat prose mid-clarification. 2. Framework deck preview off-screen: `injectDeckBridge` injected `place-content: center !important` on `.deck-shell` for every deck-mode srcdoc, which forced the framework's `display: grid` shell to re-center its implicit track. The framework's `fit()` already centers a `transform-origin: top left` stage with an explicit `translate(tx, ty)` that assumes the stage's natural layout position is (0, 0); the two centerings stacked and the scaled stage landed ~1000px off-screen, so the preview showed a sliver of slide content in the top-left with the rest black. Skip the override when the framework's `id="deck-stage"` marker is in the doc, and drop the dead `display: grid; place-items: center` from the deck framework template so future drift can't re-introduce the same stack. |
||
|
|
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> |
||
|
|
b409e5b923 |
docs(blog): refresh daily indexing status (#2346)
Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com> |
||
|
|
1f2a818c02 |
[codex] Restore chat message role headers and user bubbles (#2382)
* Restore chat message role headers * Restore user message bubble styling |
||
|
|
d03b8fd095 |
fix(plugins): stop recommending raw publish CLIs from authoring summary (#2380)
QA repro (transcript shared on PR #2363): the agent finishes the Home "Create plugin" → plugin-authoring chip flow, validates / packs / installs the generated plugin, then in its summary turn freeform- recommends shell commands like: od plugin publish ./generated-plugin --to open-design cd generated-plugin && git init && git add . && git commit -m "..." gh repo create lefarcen/<name> --public --source=. --push Two things go wrong: 1. The summary recreates the exact flows the plugin-folder card buttons already drive end-to-end (PR #2363 wired the buttons through the right gh + git sequences with auth gates, jq fallback, and retry rules baked in). The freeform suggestions drop those guarantees — they're the source of bug #2332 where `od plugin publish --to open-design` was the recommended action even though it produces an issue URL rather than creating a public repo. 2. The prompt explicitly hinted at `od plugin publish` in step 3 ("Then run or prepare the CLI path: ... and `od plugin publish` when the user is ready to open a registry PR"). The agent dutifully repeated that as the next step, even though the publish work belongs to the plugin-folder card button prompts now. Rewrites `PLUGIN_AUTHORING_PROMPT_TEMPLATE` to: * Keep the original scaffolding instructions (generated-plugin/ + SKILL.md + open-design.json + plugin.repo + inputs). * Replace the loose "od plugin whoami/login through gh, and od plugin publish when the user is ready" sentence with a precise local- validation chain: `od plugin validate` → `od plugin pack` → `od plugin install --source <abs-path>`. * Tell the agent to STOP after the summary, and explicitly ban follow-up suggestions of `od plugin publish`, `od plugin publish --to open-design`, `gh repo create`, `git init` / `git remote add` / `git push`. The ban names the workarounds it has actually emitted in QA transcripts. * Point the user at the plugin-folder card buttons (Add to My plugins / Publish repo / Open Design PR) and call out that those prompts encode the auth gates and fallback rules the summary suggestions would skip. * Same jq guidance carried over from PR #2363: do not assume the standalone `jq` binary, prefer the built-in Read tool / cat / node -e, and disambiguate from `gh ... --jq` (which is fine because gh ships its own embedded library). Tests: * `apps/web/tests/components/home-hero/plugin-authoring-prompt.test.ts` (new) — nine assertions covering: goal-placeholder interpolation, generated-plugin scaffolding mentions, local validation chain, the follow-up CLI ban list (`od plugin publish --to open-design`, `gh repo create`, `git push`), the plugin-folder card hand-off, the jq fallback + gh-flag carve-out, and the round-trip helpers (`buildPluginAuthoringInputs` / `buildPluginAuthoringPromptForInputs` / `createPluginAuthoringHandoff`). Validation: * `pnpm --filter @open-design/web exec vitest run tests/components/home-hero/plugin-authoring-prompt.test.ts` → 9/9 passed * `pnpm --filter @open-design/web exec vitest run tests/components/HomeView.prefill.test.tsx` → 11/11 passed (no regression in the adjacent HomeView prompt-prefill suite; that test references the template literal directly so a silent shape change would surface here) Related: PR #2363 fixes the same source-of-bug shape on the plugin-folder card "Publish repo" button. That PR is the canonical home for the publish flow; this PR makes sure the authoring summary hands off to it rather than re-implementing it inline. |
||
|
|
c617e30e27 |
fix(plugins): make Publish repo actually create the author's repo (#2332) (#2363)
* fix(plugins): make Publish repo actually create the author's repo (#2332) QA repro from 0.8.0 preview: clicking "Publish repo" on a generated plugin's `DesignFilesPanel` card ran the agent down a path that produced an Open Design registry-submission URL but never created the author's GitHub repo. After the action finished, `gh repo view nuomi/cat` still returned 404 and `git ls-remote https://github.com/nuomi/cat.git HEAD` failed with "Repository not found". Root cause is the action prompt at `apps/web/src/components/design-files/pluginFolderActions.ts:11-12`: publish: 'Use the supported `od plugin publish` or repository-publish flow after confirming the manifest.' That sentence let the agent pick the legacy registry-link CLI (`od plugin publish --to open-design`), which mirrors the path the "Open Design PR" button takes and emits an issue URL instead of creating a public repo. The button label said "Publish repo" but the behavior collapsed onto the registry-submission flow. This PR rewrites the `publish` prompt in the same shape PR #2182 used for `contribute` — a numbered gh + git sequence that drives the real action end-to-end: 1. Pre-flight `gh --version` / `gh auth status`. Invalid / expired tokens are treated the same as not-logged-in (the bug-report agent kept going past an "invalid token" warning). 2. Read manifest, capture `name`, `version`, `description`, `plugin.repo`. Fall back to `https://github.com/<gh-login>/<name>` when `plugin.repo` is missing and write it back into the manifest. 3. `gh repo view <owner>/<name>` to decide create-vs-update. 4a. Repo does not exist → `git init` + commit + tag + `gh repo create <owner>/<name> --public --source . --push`. 4b. Repo exists → reuse the remote, `git add -A` + `git commit -m "Update: <name> v<version>"` (skip if working tree is clean), `git tag v<version>` (skip if already published), `git push`. 5. Verify with `gh repo view <owner>/<name> --json url,nameWithOwner`. 6. Hand off the resolved `https://github.com/<owner>/<name>` URL to chat. End the turn. Hard constraints encoded in the prompt: * Do NOT call `od plugin publish --to open-design` (or any `--to <catalog>` variant). That is the registry-submission flow. * Do NOT call `AskUserQuestion` — fire-and-forget, same as the `contribute` flow's stall fix. * Do NOT auto-install gh/git. Detect-and-instruct only. * Do NOT force-push or overwrite a published tag. * Do NOT retry a failed step. Report and stop. Refactor: pulled `publish` out of the shared `ACTION_TITLES`/ `ACTION_NOTES` template into its own `buildPublishPrompt(folderPath)` function (mirrors `buildContributePrompt` from PR #2182). `install` keeps the simple shared template — that action stays inferrable from the manifest and doesn't need the same blast radius. Tests: * `apps/web/tests/components/pluginFolderActions.test.ts` — extends the existing contract suite with seven new `publish` assertions: targets `plugin.repo` not the registry catalog, drives the full gh + git command list, handles both new-repo and existing-repo branches, explicit ban on the registry-submission CLI, hard-bans on AskUserQuestion / auto-install / force-push / retry, "invalid token" treated as STOP, `${folderPath}` interpolation guard, ends by handing the repo URL back to chat. Validation: * `pnpm --filter @open-design/web exec vitest run tests/components/pluginFolderActions.test.ts` → 16/16 passed (was 9/9 before this PR; +7 new publish-flow assertions, the old generic "mentions od plugin publish" assertion replaced with the precise contract above) (Local `pnpm --filter @open-design/web typecheck` fails on `tests/runtime/exports.test.ts` because `packages/host/dist/testing` isn't built in this checkout — pre-existing breakage from `2c128e0e refactor desktop host bridge` on main, unrelated to this prompt change. CI runs a fresh install and was green on the four previous prompt-only PRs that touched the same module.) Closes #2332. * fix(plugins): don't assume standalone jq when reading the manifest QA repro from the Open Design PR button (transcript shared with the PR #2363 thread): the agent reached step 2 of the contribute prompt, ran `jq '{name,title,description,version}' generated-plugin/open-design.json`, got `zsh:1: command not found: jq`, and stopped per the prompt's "stop on first hard failure" rule. No fork, no branch, no PR. jq is not part of the OD agent runtime baseline — default macOS and Windows shells don't ship it. The agent reached for it first because "jq" is the default JSON tool in claude/codex's shell training distribution, not because the prompt asked for it. The prompt just said "Load and capture", which the agent interpreted as "shell out to the most common JSON parser". Updates both step-2 instructions (contribute + publish prompts) to: - List portable manifest-read alternatives in priority order: the built-in Read tool (always available); `cat` + manual JSON parsing; `node -e 'JSON.parse(...)'` as the shell-only fallback. - Add an explicit "Do not assume the standalone `jq` binary is installed" guard with the macOS / Windows shell rationale. - Disambiguate the standalone `jq` CLI from `gh ... --jq`. The gh flag uses an embedded library and is fine — without the disambiguation the agent reads the ban literally and stops using `gh api user --jq .login` at step 3. Tests: * `apps/web/tests/components/pluginFolderActions.test.ts` — two new contract assertions: - publish prompt: warns against assuming standalone jq is installed; lists cat and node -e as alternatives. Closes the regression on its own surface. - shared block: both contribute and publish prompts disambiguate standalone jq from `gh ... --jq`. One assertion guards both flows so a future prose edit can't drop the carve-out on one side. Validation: * `pnpm --filter @open-design/web exec vitest run tests/components/pluginFolderActions.test.ts` → 18/18 passed (was 16/16 on the previous PR #2363 commit; +2 new jq-guidance assertions) Continues PR #2363. Same source-of-bug shape as the registry-submission fallback issue this PR was opened to fix: agent picks a tool the prompt didn't actually ask for because the prompt was loose. |
||
|
|
aa8f02dbac |
chore(deps): upgrade posthog-node 4.18.0 -> 5.34.6 in daemon (#2309)
Breaking changes addressed: - posthog-node v5 replaces axios/follow-redirects/proxy-from-env with @posthog/core (native fetch); no call-site changes required — the PostHog constructor signature, capture(), identify(), groupIdentify(), on(), and shutdown() surface used by apps/daemon/src/analytics.ts is stable across the major boundary. - shutdown() is still async in @posthog/core (PostHogCoreStateless base class); the IPostHog interface in posthog-node types it as void but the inherited Promise<void> from @posthog/core keeps await client.shutdown() correct at runtime. - protobufjs resolved version: 7.5.7 (pre-existing; posthog-node v5 does not pull in @opentelemetry, so no change to protobufjs from this bump). |
||
|
|
fd2e8d9352 |
fix: resolve shared frame screen paths from referrer (#2318)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
25f977c84c |
fix(web): rename FileViewer Share button label to Export (#2233)
* fix(web): rename FileViewer Share button label to Export The toolbar button in FileViewer triggers a menu where 5 of 8 items are Export/Download actions and only Deploy to Vercel + Copy link are real "share" semantics. The previous "Share" label mismatched user mental model (Share = send a link to someone, Export = save a file locally), which likely contributed to a steep studio_view -> studio_click drop in the artifact-export funnel (~18% step conversion). Rename only the i18n value for fileViewer.shareLabel across all 18 locales so the button reads "Export" (and the locale-equivalent). Keys, component code, icon, and menu contents are unchanged. The unused fileViewer.share key and the unrelated examples.shareMenu / preview.shareMenu keys are left intact. Co-authored-by: Cursor <cursoragent@cursor.com> * Improve export CTA and BYOK test feedback * Fix BYOK provider review regressions * Fix settings locale regressions * Fix design system import header layout --------- Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
6ca4491294 |
fix(mcp): forward external MCP servers to OpenCode (#2174)
* fix(mcp): forward external MCP servers to OpenCode OpenCode (and 5 other non-Claude/non-ACP runtimes) silently dropped the user's `.od/mcp-config.json` entries at spawn time because `server.ts` only branched on `def.id === 'claude'` and `def.streamFormat === 'acp-json-rpc'`. The UI happily saved the servers and the user never learned the agent process never received them — the "ghost MCP" UX called out in #2142. Replace the two hardcoded checks with a single `def.externalMcpInjection` discriminator on `RuntimeAgentDef` (`claude-mcp-json` / `acp-merge` / `opencode-env-content`). The Claude `.mcp.json` write and ACP `mcpServers` merge paths keep their existing behavior; OpenCode now gets its config layered in via `OPENCODE_CONFIG_CONTENT`, which OpenCode merges on top of the user's saved `~/.config/opencode /opencode.json` (verified against opencode-ai 1.15.5 — `opencode mcp list` shows the injected server as `connected`). Surface the same discriminator through `AgentInfo` so the Settings → External MCP panel renders a banner naming the agents that DO receive the servers and the ones that don't, with a hint to configure those agents' own config files instead. Replaces the silent-failure UX with explicit, actionable information. Fixes #2142 * fix(web): scope external MCP banner to installed agents only mrcfps's review on #2174 pointed out that `/api/agents` returns every runtime def — including ones the user hasn't installed (those carry `available: false`) — so the support banner was happily listing Devin / Kimi / Kiro / Mistral Vibe under "Forwarded to" and DeepSeek / Pi / Qoder / Qwen under "Not forwarded to" on a machine where none of those CLIs were even present. Misleading at best, since the banner copy reads as "agents on your system." Filter the agents array by `available: true` before grouping by `externalMcpInjection`. The "no enabled MCP servers" and "daemon unreachable" short-circuits stay; add one more guard for "the user hasn't installed a single supported CLI yet" so the banner just disappears instead of showing two empty lines. Verified against the local dev runtime: on a host with 7 of the 16 known agents installed, the banner now shows the actual 3+4 split (Claude Code · Hermes · OpenCode forwarded; Codex CLI · Cursor Agent · Gemini CLI · GitHub Copilot CLI not), down from the previous 8+8 that included CLIs that don't exist on the machine. * fix(web): mark ACP runtimes as stdio-only in MCP support banner Second review pass on #2174 (mrcfps) caught that banner was treating every `acp-merge` runtime as fully forwarded, even though `buildAcpMcpServers()` in `apps/daemon/src/mcp-config.ts:386` drops every non-stdio server before spawn. Save a Higgsfield HTTP MCP and pick Hermes — daemon hands Hermes nothing, but banner still listed Hermes under "Forwarded to". The exact silent-failure UX the banner was supposed to remove. Tag ACP runtimes inline with `(stdio only)` in `renderNames`, and when at least one ACP adapter shows up in the supported group, add a one-sentence sibling explaining the limit. Pure presentation change in `McpAgentSupportBanner` — no new state, no transport-aware filtering, no contract change. Dropping the warning will be cheap to do later if ACP grows HTTP support. Verified against the local dev runtime with both a stdio (`basic-memory`) and an HTTP server (Higgsfield) saved: banner now renders "Forwarded to: Claude Code · Hermes (stdio only) · OpenCode. ACP adapters marked stdio only receive stdio MCP servers from this list; HTTP and SSE entries are dropped at spawn time." |
||
|
|
5fc27f8923 |
Fix daemon run recovery across reloads (#2374)
* 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 * Fix ProjectView daemon run recovery tests |
||
|
|
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 |
||
|
|
38b59fa4d9 |
fix(web): confirm before deleting saved template in New Project (#2330)
* fix(web): confirm before deleting saved template in New Project EntryShell and NewProjectModal were dropping the onDeleteTemplate prop, so the trash button in the template picker was inert; thread it through and gate it behind an alertdialog confirm. Fixes #2237 * fix(web/i18n): translate deleteTemplate keys across 18 locales The initial commit shipped English placeholders for the 4 new newproj.deleteTemplate* keys; replace with localized strings matching each locale's existing designs.deleteConfirm style. * fix(web): gate confirm-dialog backdrop on in-flight delete + style error message Backdrop now no-ops while `deleting` is true (matching the Cancel and Delete button gating), preventing a false-cancellation race where the backgrounded DELETE keeps running and leaks a stale `deleteError=true` into the next dialog open; also adds the missing `.modal-confirm-error` rule so the inline failure copy renders as a destructive state instead of default body styling. |
||
|
|
96b6db14c2 |
feat(landing): rebuild /templates/ catalog from design-templates (#2369)
Source the public templates page from `design-templates/*/SKILL.md` so the catalog reflects the renderable template registry (111 entries with mode, platform, and scenario metadata) instead of the previous handful of live-artifact + skill-mode shims. Render per-template thumbnails by shooting each design template's `example.html`, surface mode/platform/ scenario chips on cards and detail pages, and bump the preview navigation timeout so heavier examples no longer flake under concurrency. Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
cc0d423c89 | fix: launch windows updater fixture via node (#2364) | ||
|
|
c530d163f8 |
feat(web): "Resume conversation in new chat" UI — #462 Commit B (companion to #1718) (#2264)
* feat(contracts): add handoff request/response DTOs
Adds HandoffRequest, HandoffResponse, and HANDOFF_SCHEMA_VERSION for
the upcoming POST /api/projects/:id/handoff synthesis endpoint. Mirrors
the finalize.ts subpath pattern (package.json#exports + esbuild entry +
index re-export) so daemon and web can import
@open-design/contracts/api/handoff.
Refs nexu-io/open-design#462.
* feat(daemon): add handoff synthesis pipeline (buildHandoffPrompt + synthesizeHandoffPrompt)
Adds `apps/daemon/src/handoff-design.ts` exposing the resume-conversation
synthesis primitives the upcoming `POST /api/projects/:id/handoff` route will
call into.
- `buildHandoffPrompt({ projectId, transcriptJsonl, transcriptMessageCount,
now })` returns the system + user prompts. System prompt asks Claude to
emit a structured Markdown body with Context / Decisions made / Open
questions / Current focus / Provenance, with Provenance bullets explicitly
flat (no Markdown emphasis on labels) to preempt the PR #1584 round-2
parser bug.
- `synthesizeHandoffPrompt(db, projectsRoot, projectId, options)` reuses the
existing finalize-design pipeline pieces: `exportProjectTranscript` →
`truncateTranscriptForPrompt` → `buildHandoffPrompt` →
`callAnthropicWithRetry` → `extractDesignMd`, but without the lockfile,
disk write, design-system, or artifact-resolution paths.
- Promotes `DEFAULT_TIMEOUT_MS` in finalize-design.ts to `export const` so
handoff shares the same 120s upstream-call bound.
Refs nexu-io/open-design#462.
* feat(daemon): wire POST /api/projects/:id/handoff route
Adds the handoff HTTP route and registers it in server.ts. Validation
block + error-mapping shape mirror registerFinalizeRoutes (BYOK payload,
upstream-error → ApiErrorCode mapping, redactSecrets on the raw upstream
body). Handoff has no lockfile, so the CONFLICT branch is omitted.
`res.on('close')` is wired to flip an AbortController whose signal is
threaded into synthesizeHandoffPrompt, so a UI-side cancel actually
aborts the daemon-side Anthropic call rather than letting it keep
running after the client walks away (mirrors the PR #974 fix for
finalize).
- `apps/daemon/src/handoff-routes.ts` — new, exports registerHandoffRoutes
+ RegisterHandoffRoutesDeps.
- `apps/daemon/src/server-context.ts` — adds handoff slot to ServerContext.
- `apps/daemon/src/route-context-contract.ts` — adds RegisterHandoffRoutesDeps
to the compile-time coverage assertion.
- `apps/daemon/src/server.ts` — imports synthesizeHandoffPrompt +
registerHandoffRoutes, builds handoffDeps, registers the route next
to finalize.
- `apps/daemon/tests/handoff-route.test.ts` — 12 HTTP-layer tests:
validation (400/403/404), happy path, upstream error mapping
(401/429/502/502 non-JSON), api-key redaction.
- `apps/daemon/tests/handoff-route-abort.test.ts` — client-disconnect
aborts the daemon-side controller.
Refs nexu-io/open-design#462.
* fix(daemon): map TranscriptExportLockedError to 409 CONFLICT on handoff route
`exportProjectTranscript` acquires a per-project `.transcript.lock`
internally (apps/daemon/src/transcript-export.ts:131-163) and throws
`TranscriptExportLockedError` on EEXIST. Concurrent handoff requests —
or a handoff that races `/api/projects/:id/finalize/anthropic` — lost
that lock and surfaced as 500 INTERNAL_ERROR through the route's
generic catch.
- `apps/daemon/src/handoff-routes.ts` — catch `TranscriptExportLockedError`
and return `409 CONFLICT` ahead of the generic 500 branch, mirroring
the existing `FinalizePackageLockedError → 409 CONFLICT` mapping at
`apps/daemon/src/import-export-routes.ts:603-605`.
- `apps/daemon/src/server.ts` — thread `TranscriptExportLockedError`
through `handoffDeps` so the route can match without a direct import.
- `apps/daemon/src/handoff-design.ts` — correct the module header
comment that incorrectly claimed "no lockfile (concurrent handoff
calls are safe)" — handoff does not add its own lock, but it does
transitively acquire `.transcript.lock` via the transcript-export
call.
- `apps/daemon/tests/handoff-route.test.ts` — regression test that
pre-acquires `.transcript.lock` on disk via `fs.openSync(lockPath, 'wx')`
before firing a handoff request, asserts 409 CONFLICT.
Refs nexu-io/open-design#462 — addresses @nettee's blocking review on
PR #1718 (comment 3242251338).
* fix(daemon): keep handoff request timeout armed through the response body read
`synthesizeHandoffPrompt` cleared the upstream-call timeout in a `finally`
that ran as soon as `callAnthropicWithRetry` returned. But `fetch()`
resolves once the upstream sends *headers* — so the subsequent
`await response.json()` body read ran with no timeout. A response that
sends headers and then stalls its body could hang `/api/projects/:id/handoff`
indefinitely instead of failing.
- `apps/daemon/src/handoff-design.ts` — move `clearTimeout(timeoutId)` into a
single outer `finally` spanning both the call and the `response.json()`
body parse, so the timeout stays armed until the body is fully consumed.
- `apps/daemon/src/handoff-design.ts` — the body-parse catch now re-throws
`AbortError` as-is, mirroring the call-phase catch. Without this a
body-phase timeout would surface as `502` "non-JSON body"; re-throwing
lets the route map it to the intended `503` "handoff timed out"
(`handoff-routes.ts:122-124`).
- `apps/daemon/tests/handoff-design.test.ts` — regression test: a `fetchImpl`
returning a `Response` whose body never closes after headers, raced
against a 500ms deadline, asserts the call aborts (not hangs) and rejects
with `AbortError`.
Refs nexu-io/open-design#462 — addresses @nettee's round-2 blocking review
on PR #1718 (`handoff-design.ts:196`).
* fix(daemon): map upstream 400 to 400 BAD_REQUEST on handoff route
`callAnthropicWithRetry` preserves a non-retryable upstream status, so an
Anthropic HTTP 400 (`invalid_request_error` — unknown model, invalid
maxTokens, malformed body) reached the route's `FinalizeUpstreamError`
branch and fell through to `502 UPSTREAM_UNAVAILABLE`. That reported
deterministic caller input as a transient server outage, inviting
pointless retries and hiding which field was wrong.
- `apps/daemon/src/handoff-routes.ts` — special-case `err.status === 400`
to `400 BAD_REQUEST` with the redacted upstream detail, ahead of the
generic 502. Also refresh the route docblock: it claimed the 409 branch
was omitted (stale since the R1 TranscriptExportLockedError fix) and
that error mapping fully mirrors finalize (now diverges on 400).
- `apps/daemon/tests/handoff-route.test.ts` — route test driving an
Anthropic `400 invalid_request_error`: asserts 400 BAD_REQUEST, the
upstream detail is surfaced, and an echoed key is redacted.
- `packages/contracts/tests/package-runtime.test.ts` — import
`@open-design/contracts/api/handoff` through the package `exports` map
and assert `HANDOFF_SCHEMA_VERSION`, covering the built publish surface
(esbuild entry + exports map + root re-export) that the source-only
`handoff-contract.test.ts` does not exercise.
Refs nexu-io/open-design#462 — addresses @nettee's round-3 blocking
review on PR #1718.
* fix(daemon): await the now-async external base-URL validator on handoff route
Main's #1176 (`9a64fccd`) made `validateExternalApiBaseUrl` DNS-aware and
asynchronous (`validateBaseUrlResolved`) and updated the proxy and finalize
callers to `await` it. The handoff route — added on this branch in parallel,
against the old synchronous validator — still called it without `await`, so
`validated` was a Promise: `validated.error` / `validated.forbidden` were
`undefined`, the SSRF / malformed-URL guard silently no-opped, and a bad
`baseUrl` fell through to the upstream call and surfaced as 502.
A semantic merge break — no textual conflict, green on the branch in
isolation, red once CI re-merged latest main.
- `apps/daemon/src/handoff-routes.ts` — `await validateExternalApiBaseUrl(...)`,
mirroring the finalize route (`import-export-routes.ts:561`). The handler
is already `async`.
The existing `handoff-route.test.ts` cases "400 BAD_REQUEST when baseUrl is
not a valid URL" and "403 FORBIDDEN when baseUrl points at a private internal
IP" already encode this — red against branch + latest main, green now.
Refs nexu-io/open-design#462 — PR #1718 CI fix.
* chore(daemon): list handoff in the assertServerContextSatisfiesRoutes literal
The `assertServerContextSatisfiesRoutes({...})` call in `server.ts` enumerates
every route registrar's deps but omitted `handoff`. Adding `handoff: handoffDeps`
makes the literal complete and consistent with the other route deps.
This was not a typecheck break: route-dep coverage is guaranteed by the
`Assert<ServerContext extends AllRegisteredRouteDeps>` type in
`route-context-contract.ts` — and `AllRegisteredRouteDeps` already includes
`RegisterHandoffRoutesDeps` — not by this assertion-call literal. The literal
has omitted `handoff` since this branch's first push (`806db576`) through green
CI throughout; `tsc -p tsconfig.json --noEmit` is clean before and after.
Refs nexu-io/open-design#462 — addresses @nettee's round-4 review note on PR #1718.
* feat(web): add "Resume conversation in new chat" action (#462)
Adds a Resume control to the chat header, next to "New conversation".
Clicking it synthesizes a handoff prompt from the current transcript
via POST /api/projects/:id/handoff, opens a fresh conversation, and
auto-sends the synthesized prompt as its first user message — so a
drifted session resumes without the user replaying context by hand.
The old conversation is preserved.
- synthesizeHandoff() web-state wrapper in apps/web/src/state/projects.ts
- resume-conversation icon button in ChatPane (onResumeConversation /
resumeConversationDisabled props)
- handleResumeConversation + pendingResumeRef + auto-send effect in
ProjectView; effect gates on messagesConversationId so the prompt
cannot fire before the new conversation's message read settles
- chat.resumeConversation i18n key across all 19 locales
Commit B of #462; Commit A is the daemon endpoint (PR #1718). This
branch is stacked on feat/handoff-endpoint so the web code resolves
@open-design/contracts/api/handoff.
* fix(daemon): scope handoff to one conversation + reject empty transcripts (#462)
Addresses the review on #1718 and #2264:
- mrcfps (#2264): the handoff endpoint exported the whole project's
transcript, so a multi-conversation project blended unrelated chats
into the synthesized prompt. HandoffRequest now carries a required
conversationId; the route validates it belongs to the project
(404 CONVERSATION_NOT_FOUND), and exportProjectTranscript takes an
optional conversationId filter so only that conversation is exported.
- nettee (#1718): a zero-message conversation still called Anthropic and
fabricated a handoff. synthesizeHandoffPrompt now throws
EmptyTranscriptError on messageCount === 0; the route maps it to
400 EMPTY_TRANSCRIPT before any BYOK tokens are spent.
HANDOFF_SCHEMA_VERSION bumped to 2 (conversationId is a new required
request field). Regression tests: a two-conversation scoping test, an
empty-conversation route + pipeline test, and a transcript-export
conversationId-filter unit test.
* feat(web): send conversationId with the resume handoff request (#462)
Follows the handoff endpoint becoming conversation-scoped. The resume
flow now passes the active conversationId to POST /handoff so the
synthesized prompt summarizes only the conversation being resumed.
handleResumeConversation bails when there is no active conversation;
synthesizeHandoff and the resume tests carry the new field.
* feat(daemon): add `od project handoff` CLI + register handoff error codes (#462)
Addresses the second-round review on #1718 and #2264:
- mrcfps (#2264): per AGENTS.md "Capability exposure (UI/CLI dual-track)",
a user-facing capability must be reachable through the `od` CLI, not
only the web UI. Adds `od project handoff <id> --conversation <id>
--api-key <key> --model <model> [--base-url] [--max-tokens] [--json]`,
driving the same POST /api/projects/:id/handoff endpoint. The logic
lives in a testable handoff-cli.ts sibling module (mirrors
artifacts-cli.ts) so cli.ts's import-time dispatch stays out of tests.
- nettee (#1718): the route emitted CONVERSATION_NOT_FOUND and
EMPTY_TRANSCRIPT, which were absent from the shared API_ERROR_CODES
union. Both are now registered in packages/contracts/src/errors.ts,
with a contract test pinning them so the route and contract cannot
drift again.
A CLI contract test covers the conversation-scoped request shape,
--json output, flag validation, and daemon-error surfacing.
* fix(daemon): fail `od project handoff` on a malformed 2xx response (#462)
Addresses nettee's review on #1718: runProjectHandoff treated any 2xx
response as success, so a broken daemon/proxy 200 with malformed or
shape-invalid JSON would print `undefined` (or `{}` under --json) and
still exit 0 — breaking the fail-fast contract scripts rely on. It now
validates the body is a well-formed HandoffResponse via an
isHandoffResponse type guard and fails fast otherwise. Regression tests
cover a shape-invalid and an unparseable 200 body.
* feat(web): surface the daemon's classified handoff error in the resume toast (#462)
Addresses mrcfps's non-blocking note on #2264: synthesizeHandoff returned
null for every non-2xx response, so RATE_LIMITED, EMPTY_TRANSCRIPT, and an
upstream 400 with provider detail all collapsed into one generic "check
your API key" toast — even though handoff-routes.ts had already classified
and sanitized them.
synthesizeHandoff now returns the daemon's structured `{ error }` on a
classified failure; `null` stays reserved for a transport failure or an
unparseable body. handleResumeConversation surfaces error.message plus
redacted details for the `{ error }` case, and a distinct
daemon-unreachable message for null.
* fix(web): omit empty baseUrl from the resume handoff request (#462)
Addresses mrcfps's review on #2264: the default Anthropic config
normalizes baseUrl to '' (config.ts), and the handoff route 400s an
explicit empty baseUrl — so the Resume action failed before synthesis
for every user who never set a custom base URL.
handleResumeConversation now forwards baseUrl only when config.baseUrl
is a non-empty string, matching the contract's optional-field semantics.
Tests: the default-config path asserts baseUrl is absent from the
request, and a new case covers a custom baseUrl being forwarded.
* refactor(daemon): dispatch `od project handoff` before the generic project parser (#462)
Addresses nettee's non-blocking note on #1718: runProject ran the shared
parseFlags(PROJECT_*) before reaching the handoff switch case, so a
malformed `od project handoff` invocation (`--unknown`, `--max-tokens`
with no value) threw out of the generic parser instead of hitting
handoff-cli's structured fail() — the entrypoint behaved differently
from the unit-tested runProjectHandoff helper.
The handoff sub now short-circuits before parseFlags / projectDaemonUrl,
so `od project handoff` runs exactly runProjectHandoff with no
intervening parsing. handoff-cli.test.ts gains unknown-flag and
missing-value cases covering the structured fail path.
---------
Co-authored-by: DevForgeAI CI/CD Engineer <devforge-ai@development.ai>
|
||
|
|
204599a7ae |
feat(analytics): ship PostHog v2 event schema (#2285)
* feat(analytics): ship PostHog v2 event schema
Aligns the PostHog wire format with the product team's v2 tracking
spec (Open Design 埋点文档 2.0). The previous v1 catalogue defined a
flat per-page event name (home_view / studio_click / settings_view…);
v2 collapses everything to four core events identified through the
page_name + area + element triplet so dashboards can group by surface
without owning a separate event per page.
Key changes
- packages/contracts/src/analytics: collapse to page_view / ui_click /
surface_view / *_result event names; bump EVENT_SCHEMA_VERSION to 2;
rename the wire field anonymous_id → device_id (value unchanged);
promote the configure-state triplet (has_available_configure_cli /
configure_type / configure_availability) to a global PostHog register
so every event inherits it without per-helper boilerplate.
- apps/web/src/analytics: rewrite the 43 trackXxx helpers behind the
new typed catalogue; opt out of PostHog's built-in UA bot filter so
legitimate embedded webviews, fingerprinted browsers, and the
Playwright-based e2e runs ingest captures (the Privacy → "Share
usage data" toggle remains the single consent gate).
- apps/web components: wire P0/P1/P2 click + view + result surfaces
end-to-end — left nav, toolbar, home chat composer, recent projects,
new project modal, plugins / design systems / integrations /
automations pages, file manager, artifact toolbar/header/share popup,
feedback panel, settings sidebar / language / appearance /
notifications / pets / privacy / connectors. Fixes the v1 feedback
bug where action=clear_feedback_rating shipped rating=null instead of
the rating being cleared.
- apps/daemon: extend run_created / run_finished with the v2 context
(entry_from / project_kind / target_platforms / fidelity /
connectors / etc.), add explicit error_code classification on
result=failed (run.errorCode → AGENT_SIGNAL_* → AGENT_EXIT_* →
AGENT_TERMINATED_UNKNOWN), and read device_id from the new
x-od-analytics-device-id header. Also moves the run_created /
run_finished emission to the canonical /api/runs handler in
server.ts; the chat-routes copy was shadowed by Express's earlier
registration and never executed, which also meant run.clientType
never made it to Langfuse — fixed in the same move.
Verification
- pnpm guard / pnpm typecheck clean for daemon, web, and contracts.
- pnpm --filter @open-design/web test: 1645/1645 passing.
- End-to-end smoke through Playwright + local PostHog ingest project
420348: every page_view (home/projects/automations/design_systems/
plugins/integrations/chat_panel/file_manager), every nav element,
the new_project_modal surface_view + tab + create flow, the
plugin_replacement_modal surface_view, settings_view across nine
sections, settings_cli_test_result (codex CLI), the
project_create_result success path, and run_created + run_finished
(result=failed, error_code=AGENT_EXIT_1) all reached PostHog with
the v2 schema and the expected device_id / page_name / area /
element / fidelity / target_platforms props. The remaining
*_result events (artifact_export / feedback_submit / file_upload /
plugin_replacement / settings_byok_test / settings_connector_auth)
are wired in code; production traffic will trigger them.
* fix(analytics): preserve style category on design-systems surface chip switch
The merge resolution in DesignSystemsTab incorrectly re-introduced a
`setCategory('All')` call alongside the new `trackDesignSystemsTopClick`
emit. main intentionally keeps the active style category when the surface
filter refines within it; the regression was caught by the existing
"keeps the style category when a surface chip refines within it" test
in tests/components/DesignSystemsTab.test.tsx.
* fix(analytics): address review — senseaudio passthrough + daemon-side configure-state
Two follow-ups from the v2 schema review on #2285:
1. `byokProtocolToTracking()` was still falling through to `null` for
`senseaudio` even though the v2 BYOK provider enum now lists it. Every
`SettingsDialog` BYOK call site guards on `if (byokProviderId)`, so a
user on SenseAudio was silently dropping the provider-option,
field-focus, and test-result captures. Added the missing case so
SenseAudio gets the same analytics coverage as the other providers.
2. The daemon-authoritative `run_created` / `run_finished` events were
missing the configure-state triplet (`has_available_configure_cli` /
`configure_type` / `configure_availability`) that v2 promotes to a
global register on the web side. Daemon captures don't go through the
PostHog global register, so dashboards couldn't segment run lifecycle
by execution setup after the migration.
The fix derives the triplet server-side from `detectAgents()` and the
request's `agentId` before `design.analytics.capture(...)`:
- has_available_configure_cli: any CLI on PATH reports installed
- configure_type: 'local_cli' when the run targets an installed CLI,
otherwise 'unknown' (daemon can't see BYOK keys, which live in
web-client storage)
- configure_availability: 'available' / 'unavailable' / 'unknown'
based on the requested agent's install status, with a fallback to
'available' when any CLI is installed
This keeps the v2 schema consistent across both daemon-side and
web-side captures.
* fix(analytics): wire setConfigureGlobals so browser events carry fresh state
Third follow-up from the v2 schema review on #2285. The previous fix
addressed senseaudio + daemon-side configure-state, but reviewer flagged
that `setConfigureGlobals` was still defined-only — no caller — so every
browser-side capture inherited the boot defaults
(`has_available_configure_cli=false`, `configure_type='unknown'`,
`configure_availability='unknown'`). PostHog dashboards therefore could
not segment the new `page_view` / `ui_click` / `surface_view` events by
execution setup after a user configured their environment.
Changes:
- `packages/contracts/src/analytics/events.ts` — add a pure
`deriveConfigureGlobals(mode, agentId, agents, byokConfigured)` helper
so the web client and the daemon can derive the triplet from the same
source of truth. The helper covers all 5 `configure_type` buckets
(`local_cli` / `byok` / `both` / `none` / `unknown`) and the 3
`configure_availability` buckets (`available` / `unavailable` /
`unknown`).
- `apps/web/src/App.tsx` — add a useEffect that re-derives the triplet
whenever the user changes execution mode, selects a new CLI, saves a
BYOK key, or the detected-agent list refreshes, then pushes it to
PostHog via `analytics.setConfigureGlobals(...)`. The setter goes
through the provider so the analytics module stays the single source
of truth.
- `apps/web/src/analytics/provider.tsx` — expose
`setConfigureGlobals` on the analytics context and the test stub so
consumers route through the provider boundary.
- `apps/daemon/src/server.ts` — switch the daemon-side derive in
`/api/runs` to the shared `deriveConfigureGlobals` helper so the
authoritative run_created/run_finished captures match the web-side
payload. BYOK credentials live in the web client and stay invisible
to the daemon, so the daemon arm passes `byokConfigured: undefined`
and falls back to the installed-CLI signal.
- `apps/web/tests/analytics-configure-globals.test.ts` — new regression
test that pins the derive behavior across all branches and confirms
the setter actually mutates the client-side store. Locks the wire-up
so a future refactor can't silently turn the setter back into a
no-op.
Verification: pnpm guard clean; daemon / web typecheck clean; web tests
1703/1703 passing (up from 1696 — 7 new tests in the configure-globals
suite).
* fix(analytics): emit projects page_view + drop misattributed chat_panel source
Fourth review pass on PR #2285. Two follow-ups from mrcfps:
1. DesignsTab (projects landing) was emitting click events but no
matching page_view. Opening /projects without clicking anything left
the surface invisible in PostHog. Added a once-per-mount
trackPageView({ page_name: 'projects' }) with the same ref-keyed
pattern HomeView / PluginsView use.
2. ChatComposer was hard-coding source: 'recent_project' on every
chat_panel page_view. The web router currently only carries
projectId / conversationId / fileName, so we cannot distinguish a
New-project launch from a template-pick or a Recent-projects click
from this layer. A false constant would over-attribute every chat
launch to 'recent_project' and break the funnel slice this schema
was meant to unlock. Dropped the field for now — better no source
than the wrong source — until the router grows a launch-source
channel; the field is still defined as optional on PageViewProps so
the channel can land in a follow-up PR.
Verification: web typecheck clean; web tests 1703/1703 passing.
* fix(analytics): correct plugin-replacement async result + heterogeneous upload + missing requestId
Three follow-ups from the fifth review pass on PR #2285:
1. **plugin_replacement_result emitted before the apply settled**
(`apps/web/src/components/HomeView.tsx`). The modal's confirm action
was a synchronous wrapper around an async `usePlugin(...)` call, so
the surrounding try/catch never observed real failures and every
attempt was reported as `result=success`. Changed `PendingReplacement.
confirm` to return `Promise<void>`, made the wrapper return the
underlying promise, and moved the analytics emit into an async
IIFE in the click handler so the success/failure branches reflect
the actual outcome.
2. **file_upload_result mis-typed heterogeneous batches**
(`apps/web/src/components/FileWorkspace.tsx`). The earlier
implementation only inspected `picked[0]`, so a mixed batch like
`image.png + demo.mp4` reported `file_type=image`. Per the comment
above the block ("mixed batches collapse to other"), the
implementation now maps every file to a tracking type, collapses to
`other` when more than one distinct type is present, and falls
back to the single type otherwise.
3. **project_create_result lost the click→result correlation id**
(`apps/web/src/components/NewProjectPanel.tsx`). The click event
no longer carried the locally-generated `requestId` that
`project_create_result` keeps, so the two could not be joined.
`trackNewProjectModalElementClick()` now accepts an optional
`{ requestId }`, mirroring the other helpers, and the create-button
click threads the same id used for the result.
Verification: web typecheck clean; web tests 1703/1703 passing.
* fix(analytics): gate configure-state on agents probe + drop unsent run_created fields
Two follow-ups from the sixth review pass on PR #2285:
1. **Cold-start configure-state was stamped before fetchAgents() landed**
(`apps/web/src/App.tsx`). The useEffect that pushes the v2 triplet
into the PostHog global register fired on first paint with
`agents=[]`, so the first home/projects/plugins page_view reported
`has_available_configure_cli=false` / `configure_availability=
unavailable` even on machines that did have an installed CLI. The
effect now waits on `agentsLoading === false` and leaves the boot
defaults ('unknown'/'unknown') in place until the probe resolves.
2. **Daemon read run-context fields the web never sends**
(`apps/daemon/src/server.ts`). The daemon-side run_created /
run_finished baseProps read `projectKind`, `entryFrom`,
`projectSource`, `targetPlatforms`, `companionSurfaces`, `fidelity`,
`connectors`, `useSpeakerNotes`, `includeAnimations`,
`referenceTemplate`, and `aspect` from `req.body`, but
`packages/contracts/src/api/chat.ts` and
`apps/web/src/providers/daemon.ts` don't carry those keys on the
wire. Reading them therefore always produced null/undefined.
Dropped the unsent fields from the daemon capture; a follow-up can
extend the create payload to thread the real context through. The
`design_system_id` field stays because the chat contract does send
it.
Tests: added 3 regression tests in `tests/analytics-configure-globals.
test.ts` covering the boot-time gating contract (empty agents +
daemon mode → unavailable / local_cli; installed agent → available;
undefined agents list → unavailable). Verification: web typecheck
clean; daemon typecheck clean; web tests 1706/1706 passing (up from
1703 — 3 new cold-start tests).
* fix(analytics): pin mode='daemon' so missing-agent run reports unavailable
Eleventh review pass on PR #2285. mrcfps flagged that
`apps/daemon/src/server.ts` was calling `deriveConfigureGlobals(...)`
without `mode`, so the helper fell through to the generic branch.
Result: a run for an uninstalled agent was tagged
`configure_availability: 'available'` whenever any OTHER CLI was on
PATH, because the generic branch only looks at the cohort-wide
"any installed?" signal. That precisely undermines the slice the
daemon emit is trying to power.
The daemon's /api/runs handler is always a daemon-mode capture
(daemon is the local CLI runner — BYOK lives in the web layer), so we
now pin `mode: 'daemon'` on the call site. The helper then judges
`configure_availability` from the REQUESTED agent's install status and
reports `unavailable` when the user picked an agent that is not
installed, even if peers are.
Added a regression case in `tests/analytics-configure-globals.test.ts`:
`{ mode: 'daemon', agentId: 'codex', agents: [{claude,true},{codex,false}] }`
→ `{ has_available_configure_cli: true, configure_type: 'local_cli',
configure_availability: 'unavailable' }`.
Verification: daemon typecheck clean; web tests 1707/1707 passing
(up from 1706 — 1 new regression test).
* fix(analytics): hoist chat_panel page_view + thread requestId
- Move chat_panel page_view emit from ChatComposer to ProjectView so
it survives activeConversationId-driven ChatPane remounts. ProjectView
keys the dedupe ref by project.id; the composer drops its duplicate.
- Thread { requestId } into trackAssistantFeedbackReasonSubmitClick so
the click pairs with the existing feedback_submit_result on the same
request id (mirrors the trackNewProjectModalElementClick pattern).
* fix(analytics): keep v2 super-props alive across reset and stamp design_system_source
- Snapshot the register payload in client.ts on PostHog init and
re-register it from applyConsent(true) and applyIdentity() so a
privacy-toggle or Delete-my-data rotation does not resume capture
without event_schema_version / device_id / session_id / locale /
configure-state globals. setConfigureGlobals() also patches the
cache so a later restore picks up the current configure state.
- Stamp design_system_source on daemon-side run_created / run_finished
(it is required by RunCreatedProps / RunFinishedProps). Daemon
can't tell default vs user_selected vs inherited from the wire, so
it derives 'unknown' when designSystemId is present, 'not_applicable'
otherwise — a follow-up that threads designSystemSource through
CreateRunRequest can replace this with the precise source.
|
||
|
|
15d08d4158 | feat: add windows packaged auto update flow (#2362) | ||
|
|
1ab8758045 |
Revert "fix(web): demote Plugins and Integrations to nav rail footer (#1806)" (#2360)
This reverts commit
|
||
|
|
9122a38080 |
fix(seo): include fresh GSC data in daily report (#2343)
Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
c85da3eb40 | fix: sync landing source-of-truth paths (#2066) | ||
|
|
41e0e26d86 |
fix(web): stop re-syncing composer scroll on every keystroke (#2282)
The `draft` dep on the overlay scroll-snap effect triggered setComposerScrollTop on every keystroke, looping through the v0.8.0 mention overlay state and crashing the chat composer with Maximum update depth exceeded. Fixes #2097 |
||
|
|
81ae2b5df9 | fix(web): space comment popover actions (#2284) | ||
|
|
d72d689430 |
fix(web): preserve daemon run reattach across project switch (#2317)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ba6497b399 |
fix(web): gate srcDoc transport activation on shell ready (#2320)
Opening Tweaks could leave the HTML preview iframe blank when the
host posted `od:srcdoc-transport-activate` before the lazy transport
shell had registered its message listener. The dropped activation
was then suppressed by the dedupe check in subsequent onLoad calls,
stranding the iframe on the 536-byte empty shell.
The race surfaces after the iframe re-mounts: closing Tweaks
increments `srcDocTransportResetKey`, which re-mounts the srcDoc
iframe with a fresh shell. Re-opening Tweaks immediately fires the
`useUrlLoadPreview`-driven `activateSrcDocTransport()` while the new
shell is still loading. The post lands in a window with no listener
yet, but `activatedSrcDocTransportHtmlRef.current` is marked to the
current srcDoc, so the iframe's onLoad call later short-circuits.
Fix:
- The lazy transport shell now posts `od:srcdoc-transport-ready`
to its parent once its activate-listener is installed.
- FileViewer tracks `srcDocShellReady`, reset on iframe re-mount
and set when ready arrives (with onLoad as belt-and-suspenders).
- Activation is funneled through a new pure helper
`canActivateSrcDocTransport()` that adds the ready gate to the
existing pre-conditions. When ready flips later, the
`activateSrcDocTransport` useCallback regenerates and the
enclosing useEffect re-fires the activation cleanly.
Tests cover the shell handshake (red on main: shell never posts
ready) and the gating helper (red on main: function did not exist).
Fixes #2253
|
||
|
|
dea07840f3 |
fix: stop stale pinned todos after terminal runs (#2321)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8b16d21785 |
fix(web): coalesce chokidar rewrite bursts before refreshing files (#2326)
Agent rewrites surface to chokidar as `unlink` + `add` (+ optional
`change`) within a single tick. ProjectView refreshes the file list
on every event, so the open tab's active file vanishes for one frame
between the `unlink` and the `add`. FileWorkspace's `activeFile`
resolver returns null when the active name disappears from
`visibleFiles`, and the preview falls back to an empty state mid-run.
Add a trailing-coalesce around the file-changed refresh:
- First event arms an 80ms quiet window.
- Subsequent events inside the window reset it.
- A 250ms maxWait cap ensures a sustained edit storm still flushes.
- The intermediate `unlink` is absorbed by the next `add` and the
UI only sees a single, file-present refresh.
The coalesce is a small `useCoalescedCallback` hook so the timing
contract is unit-testable without standing up ProjectView. Tests
cover the rewrite burst, isolated single triggers, the maxWait cap
under sustained triggers, latest-callback semantics, and unmount
cleanup.
Fixes #2195
|
||
|
|
abbe5e0604 | Fix OpenCode 1.3 connection args (#2327) | ||
|
|
59a9867cf3 | fix(daemon): surface discovery form answers to agents (#2071) | ||
|
|
a89aa56db3 |
Update docs/assets/github-metrics.svg (#2312)
Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com> |
||
|
|
dc4d8cdddb |
fix(daemon): reconcile missing artifact manifests (#2068)
* fix(daemon): reconcile missing artifact manifests Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): preserve reconciled artifact mtimes Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8bcd96f5e5 |
fix(frames): resolve relative screen= against embedder URL (#2316)
Shared device frames serve at /frames/<name>.html and previously assigned the raw ?screen= value to the inner iframe.src. A project-relative value like screen=screens/foo.html resolved against /frames/, producing /frames/screens/foo.html (404), instead of the embedding project's /api/projects/:id/raw/screens/foo.html. The five frame HTML files now resolve relative ?screen= values against document.referrer when present (the embedding project preview), falling back to location.href so standalone /frames/* loads keep working. Absolute and root-relative paths are passed through unchanged. Adds an e2e Vitest spec that evaluates each frame's inline <script> in a Node vm and asserts iframe.src under five scenarios per file (25 cases total): project-relative against referrer, root-relative pass-through, absolute pass-through, empty referrer fallback, and missing ?screen= no-op. Fixes #2234 |
||
|
|
80d305858b |
feat(diagnostics): add one-click log export from Settings → About (#798)
* feat(diagnostics): add one-click log export from Settings → About
Adds a new "Export diagnostics" entry under the About section that bundles
daemon/web/desktop logs, machine info, and recent macOS crash reports into
a zip the user can share when reporting issues.
- Browser hits a new daemon HTTP endpoint and triggers a download.
- Electron uses an IPC bridge with the native save dialog and reveals the
saved file in Finder/Explorer; the Help menu also exposes it as a
fallback when the daemon is unresponsive.
Packaging + redaction lives in a new @open-design/diagnostics package so
both surfaces share it. Sensitive JSON keys, URL query secrets, and the
current user's home path are redacted before packaging.
* build(nix): include packages/diagnostics in daemon build targets
The Nix daemon derivation builds workspace siblings in dependency order
before compiling apps/daemon. Without @open-design/diagnostics in that
list, the daemon TypeScript build fails inside the Nix sandbox with
`Cannot find module '@open-design/diagnostics'` because pnpm install
only creates the symlink — the dist output that the package.json
exports point at isn't produced until each sibling's build script runs.
* build(tools-pack): include @open-design/diagnostics in packaged INTERNAL_PACKAGES
Without this, packaged win/mac/linux builds fail with `npm error 404` when
the post-build `npm install --omit=dev --no-package-lock` step in the
assembled app tries to resolve `@open-design/diagnostics@0.2.0` from the
public npm registry. The package is workspace-private, so it has to be
tarballed via `pnpm pack` and file:-referenced from the assembled
package.json like every other internal workspace dep that daemon/desktop
depend on.
Also wires the package's `pnpm --filter ... build` into the pre-pack
workspace build step so the dist/ exists before pnpm pack runs, and
updates the two test fixtures (`win-app.test.ts`, `workspace-build.test.ts`)
that mirror INTERNAL_PACKAGES.
The diagnostics package itself is repinned to exact dependency versions
already used elsewhere in the workspace (`jszip 3.10.1`, `@types/node
20.19.39`, `esbuild 0.28.0`, `typescript 5.9.3`, `vitest 4.1.6`) so it
passes the new `pnpm guard` exact-version rule and produces a minimal
lockfile diff vs main (additions only, no resolution-string churn).
* fix(diagnostics): include `~` in bearer-token redaction char class
RFC 6750 token68 syntax allows `~`, so tokens like `Authorization: Bearer
abcd~efgh` were only partially matched by `HTTP_AUTH_SCHEME_RE`. The
regex stopped at the first `~`, leaving the tail (`~efgh`) un-redacted in
the exported diagnostics zip — a clear leak since this feature explicitly
generates support bundles for external sharing.
Add `~` to the character class and a regression test.
* fix(diagnostics): only collect renderer.log from desktop
`buildSidecarLogSources` unconditionally added `logs/${app}/renderer.log`
for daemon/web/desktop, but only the desktop runtime writes a renderer
log (see apps/desktop/src/main/runtime.ts) — daemon and web are pure
Node services with no Electron renderer. Every export therefore produced
missing-file placeholders and manifest warnings for the two phantom
paths, polluting the bundle.
Gate the renderer.log source on APP_KEYS.DESKTOP so the daemon-side
collector matches the desktop-side collector in apps/desktop/src/main/
diagnostics.ts:63.
* fix(diagnostics): mirror desktop-side renderer.log gate
The previous fix only updated the daemon-side `buildSidecarLogSources`
in `apps/daemon/src/diagnostics-export.ts`. The desktop-side collector
at `apps/desktop/src/main/diagnostics.ts` had an identical copy of the
same bug that I overlooked: it also unconditionally added
`logs/${appKey}/renderer.log` for daemon/web/desktop, producing
missing-file placeholders + manifest warnings for the two phantom paths
on every desktop-initiated export.
Apply the same `appKey === APP_KEYS.DESKTOP` gate here so both export
entry points (browser via daemon HTTP, Electron via native save dialog)
emit the same clean manifest.
* feat(diagnostics): add `od diagnostics export` CLI subcommand
AGENTS.md's dual-track capability-exposure contract requires every
user-facing feature to ship on both the web UI and the `od` CLI. The
diagnostics export was only reachable through Settings → About and the
desktop Help menu; this commit closes the loop with an `od diagnostics
export [<path>] [--json]` subcommand registered in SUBCOMMAND_MAP.
The CLI is a thin shell over the existing GET /api/diagnostics/export
endpoint — same zip output, same redaction, same crash-report scope.
Defaults to writing `open-design-diagnostics-<timestamp>.zip` in the
current directory; `--output <path>` or a positional arg overrides.
`--json` prints `{path, sizeBytes}` for shell pipelines.
Use cases this unlocks:
- A CI script can `od diagnostics export ~/artifacts/bundle.zip` after
a failed run.
- Bug reporters on headless boxes can grab a bundle without booting
the web UI.
- `od doctor` follow-ups can collect a full snapshot when a probe fails.
* fix(diagnostics): surface non-sidecar launch in manifest warnings
`buildSidecarLogSources()` returns `[]` when the daemon has no sidecar
runtime context, which is the standard `od` (plain) launch path —
`runDaemonCliStartup()` -> `startDaemonRuntime()` does not pass a
runtime. Settings → About and the new `od diagnostics export` previously
reported success but produced a bundle with only the summary JSONs, so
operators could not tell "no logs because plain launch" from "no logs
because something genuinely broke."
- Extend `DiagnosticsContext` with an optional upstream `warnings:
string[]` that `buildManifest` merges into the manifest warnings.
- Emit STANDALONE_LAUNCH_WARNING from the daemon handler when
`options.runtime == null`. The warning names the limitation and
points the user at the sidecar entry points that DO capture logs.
- Add a regression spec at `apps/daemon/tests/diagnostics-export.test.ts`
that drives the handler with `runtime: null` and asserts the warning
surfaces in `summary/manifest.json` (and that `files` is empty so a
user reading the bundle does not confuse "no log sources" with
"missing files").
|
||
|
|
60bd58a1f3 | fix(nix): prebuild host package for web build (#2280) | ||
|
|
90594f674c |
fix: prevent long template names from being obscured by Official badge (#2271)
Fixes #2208 Added CSS rules to ensure long template names are properly truncated with ellipsis while keeping the Official badge visible. |
||
|
|
a0b2e17c15 |
Fix blog code blocks and add missing cover images (#2255)
* fix(landing-page): paper theme for blog code blocks Configure Shiki with a custom open-design-editorial theme so fenced code blocks render on the cream --bone paper with --ink text, matching the rest of the editorial layout. Astro's default github-dark theme was inlining background-color:#24292e on every <pre>, producing a slate- dark slab inside the warm paper page (visible on /blog/byok-design-workflow-claude-codex-qwen/ and any other post that ships a fenced block). Token colors are pulled from the same palette as globals.css (--coral, --olive, --ink-*) so syntax accents read as part of the brand instead of as a foreign widget. Belt-and-braces CSS in blog/[slug].astro pins the <pre.astro-code> background to --bone so a future Shiki upgrade cannot silently re-introduce the dark slab. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(landing-page): add missing blog cover plates Add generated editorial plate images for the layout-layer and Figma-plugin blog posts, then wire them into the blog index image map so those cards use real covers instead of fallback placeholders. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
6c87bef8ec |
fix(web): add CJK font fallback for Chinese/Japanese/Korean UI (#2227)
When switching the UI language to Chinese, Japanese, or Korean, button and label text rendered as empty boxes because the font stack lacked CJK-capable system fonts. Add a :lang() comfort pass (mirroring the existing Arabic/Persian one) with PingFang SC, Microsoft YaHei, Noto Sans SC, Hiragino Sans GB, and Source Han Sans SC. |
||
|
|
b426f614f5 |
fix(web): scope design-systems surface chip counts to active style filter (#2141)
On the Design systems page the built-in library surface chips (All / Web /
Image / …) counted the whole catalog, so applying a style category left
them showing stale totals while the grid shrank. Clicking a chip also
called setCategory('All'), discarding the user's style filter instead of
refining within it.
Surface chip counts and the grid now derive from a query-scoped set
(style category + search) over librarySystems; the chip click only
switches surface. An unscoped surfaceTotals count still backs the
"surface is empty" effect guard so it reacts to the catalog, not a
transient filter. The chip row always keeps "all" and the active surface
chip rendered, so a transient search can never hide the active filter.
Re-applied onto the DesignSystemsTab restructure from #2187. Adds 4 tests
covering scoped counts, category preservation on chip click, empty-chip
hiding, and active-chip persistence under a zero-result search.
Fixes #2062
Co-authored-by: DevForgeAI CI/CD Engineer <devforge-ai@development.ai>
|
||
|
|
210b94069a |
feat(senseaudio): BYOK chat with image + video generation tools (#2065)
* feat(senseaudio): BYOK chat with image + video generation tools
Adds SenseAudio as a first-class BYOK chat protocol and wires the daemon's
chat proxy with a tool loop so BYOK users can generate images and videos
without dropping to a CLI agent.
- BYOK protocol: new senseaudio tab + /api/proxy/senseaudio/stream route +
connection-test + provider-models discovery (OpenAI-compatible wire)
- Tool loop: generate_image (synchronous /v1/image/sync) and generate_video
(async /v1/video/create + 5s polling /v1/video/status, 10-min ceiling,
periodic progress log every 30s)
- Settings dropdown + chat-composer dropdown for the BYOK image model
default; generate_image's model enum lets the LLM override per call
- Seed-on-success: a successful BYOK chat call idempotently mirrors the
key into media-config (preserves env-resolved + already-stored keys)
- Generated artifacts land in <projectsRoot>/<projectId>/ so FileViewer,
DesignFilesPanel, and project export pick them up automatically;
legacy /api/byok-image/:id route kept for old conversation links
- Markdown renderer learns  image syntax with a scheme
allowlist (http(s) / data:image/ / blob: / relative paths)
- i18n key settings.byokImageModel across all 19 locales
- 3 SenseAudio image models registered (2.0, 1.0, doubao-seedream-5.0);
1 video model (doubao-seedance-2.0)
- Tests: byok-tools (29), media-senseaudio-image (8), media-config seed
(7), proxy-routes (47), markdown image rendering (8)
* fix(senseaudio): unblock image gen + design file preview switching
- SenseAudio /v1/image/sync rejected the previous size mapping with
`参数错误:size` (1664x936, 936x1664, 1280x960, 960x1280 are not in
the gateway's accepted set). Switched to standard HD / SD sizes that
every aspect bucket can hit: 1024×1024, 1280×720, 720×1280,
1024×768, 768×1024. Kept the byok-tools and media.ts tables in sync
so the BYOK chat tool and the CLI agent path both stop failing on
non-square aspects.
- DesignFilesPanel's <DfPreview> was missing a key prop, so React
reused the same iframe DOM node when the user picked a different
file — the src prop changed but the iframe never navigated. Added
key={previewFile.name} so the previous preview unmounts cleanly.
- Updated byok-tools + media-senseaudio-image tests for the new size
expectations.
* docs(senseaudio): clear stale provider hint + update README
- Settings → Media → SenseAudio: clear the auto-promoted
"Image · TTS · 70+ voices · clone" hint; the provider label alone is
enough now that the BYOK chat surface covers image + video tooling.
- README: list the new senseaudio (and missing ollama) proxy routes so
the BYOK section reflects what the daemon actually serves, and
mention the generate_image / generate_video chat tools that ship
with the SenseAudio path.
* fix(senseaudio): address PR #2065 review feedback
Three non-blocking review notes from @PerishCode on PR #2065:
1. Drop the dead /api/byok-image/:id route. The PR description claimed
it was "legacy fallback for old chat history" but that storage
layout never existed on main, so the route can only ever 400 or
404 — never 200. Removed the handler, the isSafeByokImageId
export, the unused createReadStream / stat / path / Request /
Response imports, and the two byok-image regression tests.
2. Add rejectProxyPluginContext guard to the senseaudio proxy
handler so it matches the invariant the other five proxy paths
already enforce (plugin runs must go through /api/runs for
snapshot pinning). Extended the existing "API fallback rejects
plugin runs" describe to also cover /api/proxy/senseaudio/stream
with the 409 PLUGIN_REQUIRES_DAEMON expectation.
3. Wrap the secondary image / video downloads (the URLs the
SenseAudio gateway hands back in /v1/image/sync .url and
/v1/video/status .video_url) in validateBaseUrlResolved so a
malicious gateway can't point us at 169.254.169.254 (AWS / Azure
metadata) or RFC1918 hosts via the response payload. Also passed
`redirect: 'error'` on both fetches to match the SSRF posture
the primary proxy fetch already uses. The new
assertExternalAssetUrl helper lives next to executeGenerateImage
so future tool downloads can reuse it.
Tests: 120/120 daemon tests pass; guard + typecheck green.
* fix(senseaudio): mirror SSRF guard onto renderSenseAudioImage CLI path
Follow-up to
|
||
|
|
431a5e2d79 |
[codex] Add global onboarding flow without AMR (#2272)
* Add global onboarding flow * Remove AMR from onboarding variant * Add onboarding role question |
||
|
|
35f9f1ce97 |
fix: improve CLI test status text wrapping to use available space (#2267)
Fixes #2247 Added word-wrap and overflow-wrap properties to .settings-test-status to ensure the status text uses the full available horizontal space before wrapping to a new line. Changes: - Added word-wrap: break-word to .settings-test-status - Added overflow-wrap: break-word to .settings-test-status This prevents premature line wrapping and ensures the CLI test status text fills the available row width efficiently, eliminating awkward empty space on the right side of the status message. |
||
|
|
ad37fd30cf | Add desktop updater UI flow (#2270) | ||
|
|
e94663bfbd | Add connector memory extraction flow (#2265) | ||
|
|
a3a238247e |
fix(workspace-tabs): keep search popover inside the viewport (#1988)
* fix(workspace-tabs): keep search popover inside the viewport The search-tabs popover used `position: absolute; right: 0` relative to `.workspace-tabs-actions`, a narrow inline-flex container that sits next to the tab strip on the left side of the chrome header — not the right. With `width: min(380px, calc(100vw - 24px))`, the popover's left edge resolved well past the left edge of the window, clipping the search input and result rows against the Electron window frame (top + left). Switch to `position: fixed` anchored to the top-right of the viewport so the popover always lands inside the window regardless of where the actions group ends up on the chrome bar as tabs accumulate. `safe-area-inset-right` guards against any future inset, and `--workspace-tabs-chrome-height` (38px fallback) keeps the offset honest if the bar height ever changes. * fix(workspace-tabs): portal popover out of strip's containment block Codex review on the previous commit flagged that `position: fixed` does not actually anchor to the viewport here, because the popover is nested under `.workspace-tabs-strip`, which sets `contain: layout style`. CSS containment establishes a new containing block for fixed descendants, so the popover's `top` / `right` were still being resolved against the strip — meaning in narrower windows or denser tab layouts the popover could still drift off-screen. Portal the popover into `document.body` via `createPortal` so it lives outside the containment context and the existing fixed positioning actually targets the viewport. Outside-click detection now checks both the trigger ref (still inside the strip) and a new ref on the portaled popover, so clicks inside the popover do not count as "outside" and close it prematurely. Guarded with `typeof document !== 'undefined'` to keep the component safe under any SSR / pre-hydration path that may exist now or later. |
||
|
|
d1eb8b7bef | Fix editorial deck navigation (#2173) | ||
|
|
716f06cb73 | fix(web): allow Comment/Inspect picker to select iframe-backed components (#2254) | ||
|
|
7fc4362ba8 |
perf(landing): edge-cache HTML and precise-load thumbnails (#2235)
* perf(landing): edge-cache HTML and precise-load thumbnails Without `public/_headers` Cloudflare Pages serves every HTML with `cf-cache-status: DYNAMIC` so each request roundtrips to the Pages origin — observed TTFB 660–900ms from Seattle, worse from Asia. With `s-maxage=3600, stale-while-revalidate=86400` HTML stays cached at the edge between deploys (CF Pages auto-purges on every deploy so freshness is unchanged in practice), and `_astro/` hash bundles flip to `immutable` so the existing 4h+must-revalidate roundtrips go away. For thumbnails, native `loading="lazy"` is browser-decided — Chrome over-prefetches (1250–3000px), Safari fires near in-viewport. A new `<LazyImg>` Astro component and global IntersectionObserver (rootMargin 300px for images, 600px for videos) replaces all 10 site-wide `loading="lazy"` usages with precise control. Above-the-fold slots (first 4 rows, detail-page hero previews) opt into `eager` or `priority` to skip the IO roundtrip. Homepage hero LCP gets `<link rel="preload" imagesrcset>`, a 4-step `srcset` (768/1280/1920/2560) plus `fetchpriority="high"` so retina devices stop repainting from the 1024-only variant — was the P99 long tail. Verified: `pnpm guard` 6/6, `pnpm typecheck` 0 errors, `pnpm build` 865 pages 28s, generated `out/index.html` contains the preload link and 15 `data-precise-src` thumbnails, `out/plugins/index.html` has 95 precise-loaded thumbnails plus the IO script. * perf(landing): logo to webp + parallelize Google Fonts load Two HAR-validated wins on top of the edge-cache / precise-load commit: logo: 500x500 192KB PNG → 200x200 7.5KB WebP. Footer/header actually render at 36x36, so the source is 5x larger than necessary at the display size and ships RGBA PNG bytes for what reads as a flat graphic. WebP at q=85 keeps the gradient ring crisp at every DPR we care about. fonts: globals.css used `@import url(...)` for Google Fonts, which serialized HTML → CSS → fonts.googleapis.com/css2 → fonts.gstatic.com/ woff2. HAR measured 953ms for the fonts CSS plus 400–800ms per woff2 × 4 — close to 3s before text could render in the intended family, even with display=swap. Moving to `<link>` + `<link rel=preconnect>` in each page's <head> lets the fonts CSS fetch race the HTML body parse, and warms the TLS handshake to gstatic.com so woff2 requests don't pay DNS+TLS at request time. A shared `font-stylesheet.astro` keeps the four-family URL canonical across all five entry points (index, sub-page-layout, plugins/index, plugins/[slug], blog/index, blog/[slug]). og.astro already had this treatment. |