`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").
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.
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.
`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.
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.
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).
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.
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.
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.
* 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.
* 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.
* feat(blog): daily 3-day Search Console traffic digest
Adds `blog-3day-report.yml` (cron 09:00 Asia/Shanghai) and a
companion `report-3day.ts` script that refreshes
`docs/blog-traffic-digest.md` once per day. The digest has two
sections:
- T-3 spotlight: posts published exactly three days ago, with their
3-day Search Analytics window plus current URL Inspection coverage
state.
- Rolling 30-day cohort: every post 1–30 days old with its latest
3-day Search Analytics window, sorted by impressions descending.
The workflow is read-only against Google APIs (no Indexing API,
no "request indexing" automation) and mirrors the secret / config
plumbing already used by `blog-indexing-monitor.yml`. Output lands
in a reviewable `automation/blog-traffic-digest` PR opened by the
open-design bot.
Also widens `querySearchAnalytics` to accept `windowDays: 3 | 7 | 28`
and updates `docs/blog-indexing-automation.md` with the new pipeline.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(seo): post daily Search Console report to Feishu
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(blog): push traffic digest to Feishu
Emit a compact JSON summary from the daily 3-day traffic digest and add a Feishu custom bot sender for the summary card. Wire the workflow to send the card when `FEISHU_BLOG_DIGEST_WEBHOOK` is configured while keeping Markdown PR output as the source of truth.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(landing-page): add Discord routing CTAs
Add a lightweight Discord pill to the landing hero and Discord links in the landing and blog footers so community routing is visible without displacing the primary GitHub and download CTAs.
Add a blog-ending conversion card that points guide and use-case readers to the internal workflows library, while keeping Discord as a secondary support path.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix links for nvm and fnm in QUICKSTART.md
Updated links for nvm and fnm in QUICKSTART.md.
* docs: point Quickstart version manager links upstream
---------
Co-authored-by: ayumi <ayuday@163.com>
When a user picks a plugin chip on Home (e.g. "Web prototype") and sets
fidelity / artifactKind / audience / etc. before opening the project,
the daemon already renders those choices into the system prompt as a
`## Active plugin` / `## Plugin inputs` block — see
`docs/plugins-spec.md` §1258. The Quick-brief form, however, still
asked the user the same questions a second time:
> "在输入框当中已经选了高保真和 Web,但在 Chat 生成的实时选择面板里面又要选一次。"
Root cause is a prompt scoping bug: RULE 1's "drop matching default
questions" rule only pointed at the `## Project metadata` section, not
the `## Plugin inputs` block. Plugin-trigger projects ship most of
their brief through `## Plugin inputs`, so the agent kept the defaults
(Target platform, Fidelity, …) even though every answer was right there
in the same prompt one section over.
Fix: extend RULE 1's source-of-truth list to both blocks, with an
explicit "equally authoritative" line and a small mapping table the
agent can apply by name:
* `fidelity` → Fidelity
* `platform` / `surface` / `platformTargets` / `target` → Target platform
* `artifactKind` / `mode` / `taskKind` → "What are we making?"
* `audience` → "Who is this for?"
* `designSystem` / `brand` → Brand context
Adjacent carve-out updated too: the "don't re-ask the kind itself"
sentence now triggers when the active plugin's `od.kind` /
`taskKind` already names the kind, not only when `metadata.kind` is
set. Closes the loop for chip-launched flows where metadata.kind is
set by the daemon but the form was still re-asking.
Why prompt-only (not a data-flow fix):
`## Plugin inputs` is already a first-class block the daemon writes
into every chip-launched run (`apps/daemon/src/prompts/system.ts:246`,
plugins-spec.md §1258). Mapping plugin inputs to `project.metadata`
fields at create time was the other option considered, but it would
require a per-plugin mapping convention (which input name maps to
which metadata field) and would have to be rolled out across web,
daemon, schema, and every existing plugin manifest. The agent already
sees the inputs; the prompt just wasn't telling it those inputs count
as answers. Cheapest fix that generalises to every plugin.
Tests:
* `apps/daemon/tests/prompts/discovery-plugin-inputs.test.ts` (new) —
five contract tests locking the new rule: the dual-source
instruction, the "equally authoritative" line, the canonical
input→question mappings (`fidelity` / `platform` / `artifactKind` /
`audience` / `designSystem`), the semantic-equivalence aside for
alternate input names, and the kind carve-out extension to the
active plugin's `od.kind` / `taskKind`.
Validation:
* `pnpm --filter @open-design/daemon exec vitest run tests/prompts/discovery-plugin-inputs.test.ts`
→ 5/5 passed
* `pnpm --filter @open-design/daemon exec vitest run tests/prompts/system.test.ts tests/prompts/api-mode-override.test.ts`
→ 34/34 passed (no regressions)
Fixes#2225
Added word-wrap and overflow-wrap properties to .qf-help to ensure
long helper text wraps properly instead of being truncated in the
narrow left panel.
Changes:
- Added word-wrap: break-word to .qf-help
- Added overflow-wrap: break-word to .qf-help
- Added line-height: 1.4 for better readability of wrapped text
The fix ensures helper text remains fully readable in the question
form without requiring panel width changes or tooltips.
* feat(web): improve manual edit UX with focus mode, uploads and remove-element patch
* fix: address review feedback on manual edit UX
- Remove unused panel width persistence (leftPanelRef, rightPanelRef props, constants, useEffects)
- Fix image upload to use project-relative URLs instead of daemon API URLs for deploy/share
- Add i18n keys for new UI strings (deleteElement, uploadImage, focusSlides, etc)
- Improve delete guard to prevent removing last document element
- Use translated strings instead of hardcoded English in all new buttons/labels/dialogs
- Fix upload error message to use i18n
* fix: add onPickImage prop and delete-element UI to ManualEditPanel
- Declare onPickImage?: (file: File) => Promise<string | null> in prop interface
- Destructure onApplyPatch and onPickImage from props
- Add image upload section with file input, uploads via onPickImage,
applies set-image patch on success, shows uploadingImage state
- Add delete element button with two-step confirmation that emits
remove-element patch via onApplyPatch
- All new strings routed through t() using existing i18n keys
* fix(web): harden manual edit delete/upload flows
* fix(web): gate image upload to image targets and normalize relative asset refs
---------
Co-authored-by: Jose Herrera <nombreregular@gmail.com>
* fix(daemon): ensure node binary dir is on PATH for agent sub-processes on Windows
When pnpm tools-dev launches the daemon via a full path to node.exe the
nodejs directory may not appear in process.env.PATH. Agent .cmd shims on
Windows (e.g. the npm copilot shim) call `"node" script.js`, so cmd.exe
fails to find node and the run exits immediately with:
'"node"' is not recognized as an internal or external command
Fix: in createAgentRuntimeEnv, prepend path.dirname(nodeBin) to PATH
when that directory is not already present. This is a no-op on systems
where the node directory is already in PATH.
Also extend the stdin 'error' swallow to cover the Windows equivalent of
EPIPE: err.code === 'EOF' / err.message === 'write EOF' (libuv UV_EOF).
These fire when the child process exits before reading stdin and were
incorrectly surfaced to the user as AGENT_EXECUTION_FAILED.
Affected file: apps/daemon/src/server.ts
* fix(daemon): anchor PATH-prepend test assertions and add coverage for both branches
- Update the existing 'injects daemon URL and run-scoped tool token' test to
expect PATH = `/opt/open-design/bin${path.delimiter}/bin` after the prepend,
matching the behaviour introduced in the Windows PATH fix.
- Add 'prepends node binary directory to PATH when not already present': given
PATH='/bin' and nodeBin='/opt/node/node', asserts PATH becomes
`/opt/node${path.delimiter}/bin`.
- Add 'does not duplicate node binary directory when already present in PATH':
given PATH already contains the bin dir, asserts it is unchanged (idempotent).
- Replace parts.includes(nodeBinDir) with a normalised some() predicate that
strips trailing path separators and performs a case-insensitive comparison on
win32, producing true idempotence when PATH contains the same directory with a
different case or trailing slash.
* case-insensitive key path finding
* fix Case-insensitive key lookup and add test
* fix(daemon): inject Node binary dir into all agent launch/probe envs on Windows
applyAgentLaunchEnv() is the shared terminal step for every agent spawn
path — detectAgents(), connection tests, and chat runs. Add an optional
`nodeBinDir` parameter (default: path.dirname(process.execPath)) so the
running Node directory is prepended to PATH at every call site without
requiring changes to detection.ts or connectionTest.ts.
Add two regression tests: a cross-platform test covering the Windows-style
'Path' key-casing fix, and a Windows-only test using proper C::\ paths and
';' delimiter verifying the full end-to-end Windows behavior.
* test(daemon): fix applyAgentLaunchEnv tests broken by nodeBinDir default
The 4 tests added in agent-runtime-env.test.ts used the 2-argument form of
applyAgentLaunchEnv, picking up path.dirname(process.execPath) as the new
default and making assertions non-deterministic across CI runners. Also,
three of the fixtures mixed Windows C:\...;... paths with POSIX path.delimiter
(':'), which silently produced wrong splits on POSIX runners.
Pass '' as nodeBinDir to all 4 tests so the early-return and path-prepend
logic is exercised in isolation. Replace Windows-style fixtures with
POSIX paths + path.delimiter; the real C:\... end-to-end shape is already
covered by winTest in tests/runtimes/launch.test.ts.
When the user enters a project from a free-form prompt (no Home chip
selected, so the active plugin is `od-default`), the agent currently
emits two clarification cards back-to-back:
* turn 1 → `<question-form id="task-type" title="Choose the task type">`
asking what to build + any constraints
* turn 2 → `<question-form id="discovery" title="Quick brief — 30
seconds">` asking audience / platform / tone / brand / scale /
constraints
Two cards in a row reads as "the agent is re-asking" and forces the user
to context-switch twice before anything gets built. The fix is to make
the turn-1 `task-type` form a single-shot brief — it now also asks for
audience, brand (with the three branch values RULE 2 dispatches on),
scale, and constraints in the same card. After `[form answers —
task-type]` the agent skips the Quick brief form entirely and goes
straight to RULE 2 brand handling, then RULE 3 planning.
Mechanics:
* `apps/daemon/src/prompts/discovery.ts` line 44 — RULE 1's
default-router exception now explicitly says "do NOT emit a second
`<question-form id="discovery">` / 'Quick brief — 30 seconds' form".
Earlier wording said "continue with the normal discovery / plan /
generate / critique flow" which the agent read as "emit the normal
discovery form next turn".
* `apps/daemon/src/prompts/discovery.ts` line 47-99 — task-type form
grows from 2 questions (taskType + constraints) to 5: taskType,
audience, brand, scale, constraints. `platform` and `tone` from the
old Quick brief are deliberately dropped — platform is
task-type-specific (only matters for prototype/live), tone is fluffy
and the agent can usually infer it from brand anyway. The agent can
still ask one targeted follow-up under the "Ask at most one concise
follow-up only if a required detail is impossible to infer safely"
rule already in `system.ts:143`.
* `apps/daemon/src/prompts/discovery.ts` line 132 — RULE 2's first
sentence now accepts `[form answers — task-type]` alongside `[form
answers — discovery]`, so the brand-branching logic in Branches A/B
works the same way for both entry points.
Non-default-router projects (the user did pick a Home chip → the active
plugin is a specific scenario, not od-default) are unchanged. They still
get the single `<question-form id="discovery">` Quick brief on turn 1,
because the chip already supplied the task type.
Tests:
* `apps/daemon/tests/prompts/discovery-form.test.ts` (new) — 5 contract
tests locking the single-shot shape: task-type form contains all five
brief fields, preserves the three `brand` branch values RULE 2 keys
off of, keeps the eight canonical task-type options, embeds the
explicit "do NOT emit a second form" sentence, and teaches RULE 2 to
accept the task-type answer marker.
Validation:
* `pnpm --filter @open-design/daemon exec vitest run tests/prompts/discovery-form.test.ts`
→ 5/5 passed
* `pnpm --filter @open-design/daemon exec vitest run tests/prompts/system.test.ts tests/prompts/api-mode-override.test.ts tests/plugins-bundled-scenarios-roster.test.ts tests/plugins-scenario-fallback.test.ts`
→ 55/55 passed (no regressions in adjacent suites)
* feat(web): introduce Automations tab with dual-track capability for routines
This commit adds a new Automations tab that consolidates routines, schedules, and live artifacts, allowing users to manage automations seamlessly. The tab features a modal for creating and editing automations, which supports various scheduling options (hourly, daily, weekdays, weekly) and project modes (create_each_run, reuse). The CLI is also updated to expose automation commands, ensuring consistency between the web UI and CLI interfaces.
Key changes include:
- New `NewAutomationModal` component for automation creation and editing.
- Updated `TasksView` to integrate the new Automations functionality.
- Enhanced styling for the Automations tab to improve user experience.
This implementation aligns with the dual-track capability exposure policy, ensuring all features are accessible via both the web UI and CLI.
* feat(daemon): enhance automation context handling and CLI commands
This commit introduces several improvements to the automation context management and updates the CLI commands accordingly. Key changes include:
- Added support for new context fields (`plugin`, `mcp`, `connector`) in automation commands.
- Updated the CLI to reflect new target options (`new-project`).
- Enhanced error messages for invalid target inputs.
- Introduced functions to handle context selection and normalization for routines, including the ability to parse and store context data in the database.
- Updated the database schema to include a new `context_json` field for routines.
- Improved the handling of context in routine routes and the web interface, ensuring that selected contexts are properly managed and displayed.
These changes aim to provide a more robust and flexible automation experience, aligning with the recent enhancements in the web UI.
* feat(web): enhance TasksView with automation run history and status indicators
This commit introduces several new features to the TasksView component, including:
- Added functionality to display automation run history for each routine, showing metadata such as status, timestamps, and project details.
- Implemented status indicators for routine runs, providing visual feedback on their current state (succeeded, failed, running, queued).
- Enhanced the UI to allow users to expand and view detailed run history, including the ability to open the corresponding project conversation.
- Updated styles to improve the presentation of automation statuses and history.
These changes aim to provide users with better insights into their automation routines and improve overall usability.
* feat(daemon): implement automation ingestion and proposal management
This commit introduces several new features related to automation ingestion and proposal management within the daemon. Key changes include:
- Added new modules for handling automation source packets and proposals, allowing for the storage, retrieval, and management of automation-related data.
- Implemented functions to list, create, and apply automation proposals, enhancing the automation workflow.
- Introduced new CLI commands for interacting with memory entries and automation sources, providing users with more control over their automation processes.
- Enhanced the server routes to support automation source and proposal APIs, enabling seamless integration with the existing system.
These changes aim to improve the overall automation experience, making it easier for users to manage and utilize automation proposals and ingestions effectively.
* Preserve HTML preview state across mode toggles
HTML previews could rebuild their iframe when switching into Edit or Comment, which reset scroll/canvas state and caused visible churn for multi-file artifacts. The viewer now keeps URL-loaded previews mounted when the artifact owns the mode bridge, relays file-refreshes through frame navigation, and restores preview scroll/viewport state across bridge mode changes.
Constraint: Generic srcdoc-only bridges are still required for unbridged artifacts, inspect mode, palette tweaks, decks, draw overlays, and forceInline.
Rejected: Keep all Edit/Comment previews on srcdoc | causes unnecessary iframe replacement for bridge-capable URL-loaded artifacts.
Confidence: high
Scope-risk: moderate
Directive: Do not enable URL-load for bridge-dependent modes unless the artifact has an owned postMessage bridge.
Tested: pnpm guard
Tested: pnpm --filter @open-design/web typecheck
Tested: pnpm --filter @open-design/web test
Tested: Playwright verified Edit and Comment toggles preserve iframe src and DOM node while receiving comment targets.
* Prevent preview wheel gestures from escaping into zoom
Trackpad pinch-like wheel events arrive with ctrl/meta modifiers on some platforms, which can make a normal vertical scroll feel like the preview zoomed. The preview now consumes those modified wheel events inside the host preview shell and in injected srcdoc previews, then maps the delta back to scroll where a scroll target exists.
Constraint: URL-loaded sandbox iframes cannot always be inspected by the host, so srcdoc previews need their own in-frame guard.
Rejected: Add allow-same-origin to preview iframes | weakens the sandbox boundary for generated artifacts.
Confidence: medium
Scope-risk: narrow
Directive: Do not broaden iframe sandbox permissions to fix gesture handling without a security review.
Tested: pnpm guard
Tested: pnpm --filter @open-design/web typecheck
Tested: pnpm --filter @open-design/web exec vitest run tests/components/FileViewer.test.tsx tests/runtime/srcdoc.test.ts
Tested: playwright-cli verified ctrl-wheel in preview keeps app zoom at 100% and prevents default in the iframe context
* Revert "Prevent preview wheel gestures from escaping into zoom"
This reverts commit 976407ab4c.
---------
Co-authored-by: nicejames <nicejames@gmail.com>
Two-part fix for the Plugin folder card's "Open Design PR" button. The
flow was broken end-to-end: the CLI emitted a 404 URL, and the agent
prompt under-specified the contribution steps so the agent stalled
mid-turn or fell back to the legacy issue-URL path.
Catalog target — `apps/daemon/src/plugins/publish.ts`:
`buildPublishLink({ catalog: 'open-design' })` hardcoded a submission URL
at `github.com/open-design/plugin-registry`, the dedicated registry repo
proposed in docs/plans/plugin-registry.md §1.2. That repo doesn't exist
yet (P3.1 notes "creating the external GitHub repo is an operational
launch step, not a code blocker"), so every generated URL 404'd. Retarget
the catalog at the live `nexu-io/open-design` monorepo and update the
target-path hint in the PR body to `plugins/community/<plugin-name>/`
(the actual layout under main). Plan §1.2 stays the long-term goal — see
the code comment.
Also updates the matching `od plugin yank` issue URL in cli.ts.
Agent prompt — `apps/web/src/components/design-files/pluginFolderActions.ts`:
The `contribute` action prompt only said "use the supported `od plugin
publish` Open Design registry flow", which produces an issue URL (the
legacy path) and left the agent to invent the remaining steps. The agent
ended up calling `AskUserQuestion` mid-turn waiting for input that the
DesignFilesPanel buttons couldn't satisfy, then stalled for 600s.
Rewrite the `contribute` prompt to drive the full PR flow via raw `gh`
commands:
1. preflight `gh --version` / `gh auth status` (detect-and-instruct on
missing CLI, never auto-install anything)
2. read manifest, resolve author login
3. `gh repo fork nexu-io/open-design --remote=false`
4. clone fork, branch, cp plugin into `plugins/community/<name>/`,
commit/push using author's git identity (not a hardcoded bot)
5. `gh pr create ... --web` so the author reviews and clicks Create
Hard bans on `AskUserQuestion`, retry-on-failure, and the legacy
`od plugin publish --to open-design` CLI keep the turn fire-and-forget.
The `install` / `publish` action prompts are unchanged.
Tests:
* `apps/daemon/tests/plugins-publish.test.ts` — assert URL host +
catalogLabel match `nexu-io/open-design`, body contains the new path
hint.
* `apps/web/tests/components/pluginFolderActions.test.ts` (new) — lock
the contribute prompt's command surface, the `--web` review window,
the `AskUserQuestion` ban, the install-tool ban, and folder-path
interpolation. Nine assertions covering the prompt contract without
coupling to exact wording.
Validation:
* `pnpm --filter @open-design/daemon exec vitest run tests/plugins-publish.test.ts`
→ 11/11 passed
* `pnpm --filter @open-design/web exec vitest run tests/components/pluginFolderActions.test.ts`
→ 9/9 passed
* `pnpm --filter @open-design/web typecheck` clean
* E2E: button click in DesignFilesPanel under namespace=e2e-pr-test ran
`gh --version` → fork → clone → branch → commit/push → ended at
`gh pr create --web` opening the GitHub PR draft in browser. No
AskUserQuestion stall this time.
Doesn't touch: `5f71968f` server-side endpoints (`/contribute-open-design`
+ `/publish-github` still in main as dead code — separate cleanup),
plan/spec docs (per maintainer call to keep the dedicated-repo as the
long-term goal), or `registry-backends.test.ts` (plan terminal-state
fixture; no production caller).
* feat(privacy): collapse first-run banner to a single "I get it" button
Replaces the first-run privacy disclosure's two-button decision picker
("Share usage data" / "Don't share") with a single "I get it"
acknowledgement. Clicking it accepts the same default telemetry surface
the previous "Share usage data" path enabled — the banner shifts from
binary consent picker to informed disclosure.
To keep the surface honest, the banner footer is rewritten to spell out
the new default and point at the off switch:
"Data sharing is on by default. You can turn it off any time in
Settings → Privacy. We never upload the contents of your generated
artifact files."
Settings → Privacy (PrivacySection.tsx) is unchanged — that surface still
exposes both Share and Don't share buttons so users who arrive there
later (or come back to flip the choice) keep the explicit picker.
Mechanics:
* `PrivacyConsentModal.tsx`: drop `onDecline` prop and the second button;
rename `onShare` → `onAccept` to match the new semantic. Footer hint
now reads from `settings.privacyConsentBannerFooter` (new key) so the
banner copy can speak in single-button voice without disturbing the
reused `settings.privacyConsentFooter` that PrivacySection still
displays.
* `App.tsx`: drop the `onDecline` handler. Single `onAccept` handler
applies the same opt-in payload as the previous `onShare` branch
(`telemetry.metrics = true`, `telemetry.content = true`, fresh
`installationId`), so the wire format daemon-side is unchanged.
* `i18n/types.ts` + `locales/en.ts`: two new keys —
`settings.privacyConsentAccept` ("I get it") and
`settings.privacyConsentBannerFooter` (the default-on disclosure copy).
* `i18n/locales/*.ts` (all 18 non-en dictionaries): added the two new
keys. zh-CN and zh-TW are translated; the remaining 16 locales follow
the project convention of leaving the EN string as a fallback for
later contributor passes (same shape used by privacyConsentShare /
Decline today).
* `tests/components/PrivacyConsentModal.test.tsx`: rewritten. The four
new tests lock the new contract — single "I get it" button, no
Share/Decline labels, default-on disclosure text in the footer, the
external privacy-policy link, and onAccept firing on click. Replaces
the prior "equal-prominence" tests, which only made sense for the
two-button shape.
Validation:
* `pnpm --filter @open-design/web exec vitest run tests/components/PrivacyConsentModal.test.tsx`
→ 4/4 passed
* `pnpm --filter @open-design/web exec vitest run tests/i18n/locales.test.ts`
→ 5/5 passed (every locale aligned with English keys + placeholders;
the new keys ship to all 19 dictionaries)
* `pnpm --filter @open-design/web typecheck` clean
* test(privacy): update App.connectors fixture to match single-button banner
`App.connectors.test.tsx > does not show first-run privacy consent until
daemon config hydration finishes` hardcoded the previous "Share usage
data" affirmative button label. The single-button banner now renders
"I get it", so the assertion was looking for a button that no longer
exists.
CI signal: 26081467446 → Web workspace tests → `× does not show
first-run privacy consent until daemon config hydration finishes`. The
App workspace tests + Validate workspace failures cascaded from this
one — both are aggregator jobs.
Local: vitest run tests/components/App.connectors.test.tsx → 5/5 passed.
* fix(web): demote Plugins and Integrations to nav rail footer
Plugins and Integrations are platform-configuration surfaces, not
daily-use destinations. Moving them to the footer section of the
left nav rail — separated from primary items by a thin divider —
keeps them reachable while giving the primary four items
(Home, Projects, Automations, Design Systems) the visual weight
they deserve.
- EntryNavRail: remove Plugins/Integrations from the main __group
and place them in the __footer above the help launcher
- entry-layout.css: add __divider rule (1 px separator) to visually
mark the boundary between primary and secondary nav regions
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): remove settings dropdown, gear opens settings directly
The gear/cog button previously opened a dropdown that mixed three
unrelated concerns: community links (X, Discord), preference quick-
access (Language, Appearance), a feature shortcut (Use everywhere),
and a redundant Settings entry — creating two separate paths to the
same Settings dialog and duplicating Language/Appearance relative to
the Settings sidebar.
Changes:
- Gear button now directly opens the Settings dialog (no intermediate
dropdown layer)
- Follow @nexudotio on X and Join Discord moved to the Help menu at
the bottom of the nav rail, where community/external links belong
- Language and Appearance remain exclusively in the Settings dialog
- Use everywhere remains exclusively in the topbar chip
- Remove dead state (avatarMenuOpen, languageExpanded,
appearanceExpanded), ref, outside-click effect, and module-level
constants (APPEARANCE_THEMES, APPEARANCE_LABEL, describeModelChip)
that were introduced solely to support the dropdown
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): move output-type chips above chat input as tab bar
The home hero previously had two chip rows below the chat input that
mixed two unrelated dimensions: output type (Prototype, Slide deck,
Image…) and workflow source (Create plugin, From Figma, From folder,
From template). Users had no visual cue that these were different
categories.
This change separates the two dimensions clearly:
- Output type (create group) becomes a tab bar positioned above the
input card. Tabs share the same chip data and onPickChip dispatch,
so plugin selection, active state, and pending state are unchanged.
Active tab shows a colored underline; the bar border visually
connects to the input card below.
- Workflow source (migrate group) stays as the chip row below the
input card, now standing alone with unambiguous "how to start"
semantics.
- Subtitle updated from "Pick a plugin below" to "Pick a type"
to match the new placement.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): skip replace-prompt confirmation when switching output-type tabs
Output-type tab clicks (create group) are mode-selection gestures;
the user expects the prompt to update immediately without a dialog.
The confirmation is still shown for migrate-group chips (From Figma
etc.) where the replacement carries meaningful user-provided content.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): use brand logo as Home destination, drop redundant brand mentions
The entry nav rail previously rendered the brand logo and a separate
Home icon back-to-back; both invoked `onViewChange('home')`, so the
Home button was pure duplicate affordance. The hero pane also
displayed a third "Open Design" lockup that competed with the rail
logo and the rebranded title.
This collapses those affordances:
* `EntryNavRail` drops the dedicated Home `NavButton`. The brand
logo button now carries `aria-current="page"` and an `is-active`
visual state when the home view is showing, and its tooltip reads
"Open Design · Home" off-home so the navigation behavior stays
discoverable for new users.
* `entry-layout.css` adds the matching `.entry-nav-rail__logo.is-active`
accent treatment so the "you are here" cue reads at parity with
primary rail buttons.
* `HomeHero` removes the inline `home-hero__brand` lockup and the
associated CSS, then retunes the title/subtitle/type-tab spacing
so the headline group still pairs tightly with the type tabs and
input card below.
* `entry-chrome-flows` is updated to assert the logo carries the
active page treatment and that no `entry-nav-home` test id
resurfaces by mistake.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): auto-grow home chat input, disable internal scroll and drag-resize
The home hero textarea was capped at a fixed `min-height: 84px` with
`resize: vertical`, so users had to either drag the bottom-right
corner to enlarge it or scroll the textarea internally to read
longer prompts. That hid context (loaded plugin templates routinely
overflow three lines) and exposed a manual grip whose state was easy
to leave in an awkward height.
This change makes the chat box grow with its content:
* `HomeHero` adds a `useLayoutEffect` that, on every prompt change,
resets the textarea height to `auto` so the browser can measure a
smaller content, then writes back `scrollHeight` in pixels. The
effect uses layout phase (not effect phase) to avoid a one-frame
flash at the previous height when a plugin loads a long example
prompt.
* `home-hero.css` swaps `resize: vertical` for `resize: none` and
adds `overflow: hidden`, so the manual drag grip disappears and
the textarea never scrolls internally. `min-height: 84px` is kept
so the empty input still reads as a chunky chat box. The outer
page handles overflow when the prompt is genuinely very long.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): make output-type tabs read as folder-tabs attached to chat box
The previous tab bar used a small icon + label per tab and signaled
the active tab with a 2px accent underline sitting on a horizontal
divider line. That divider visually broke the relationship between
the tabs and the input card below — the active tab looked like a
free-floating header, not a flap of the chat surface, so users had
to do extra work to mentally connect "I picked Prototype" with the
prompt area immediately underneath.
This switches the tab bar to a folder-tab pattern:
* `HomeHero` drops the per-tab `Icon` element. The seven labels
(Prototype, Live artifact, Slide deck, Image, Video, HyperFrames,
Audio) already disambiguate at the type sizes used here, and the
icons were primarily decorative.
* `home-hero.css` rewrites the tab styling:
- The bar no longer paints its own baseline border; the input
card's top edge serves as the baseline.
- Each tab is a rounded-top container with a 1px transparent
border and a `-1px` bottom margin, so its bottom edge overlaps
the card's top border by exactly one pixel.
- The active tab borrows the card's panel background and border
color, and overrides its bottom border with the panel color
so it visually erases the card's top border for the tab's
width. The result reads as one continuous "Prototype is this
chat box" surface.
- The card's prior `margin-top: 8px` is removed so the tab bar
bottom and card top sit at the same y coordinate, which is the
geometric precondition for the 1px overlap to land cleanly.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): enlarge home chat attach and submit buttons for legibility
The attach (paper-clip) and submit (arrow-up) controls on the home
chat input rendered at 32px diameter with 14px and 18px glyphs
respectively. After stroke antialiasing the icons read at roughly
11–12px on typical displays — small enough that users reported the
glyphs were illegible and had to be discovered by trial-and-error.
This bumps both controls to 38px circles and grows their glyphs
(attach 14 → 18, submit 18 → 22). The primary call-to-action now
has clearly more visual weight than the surrounding muted hint
text, and the paper-clip is recognisable at a glance.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): paint a chevron on inline select slots in the home prompt
Inline select slots in the home prompt overlay (e.g. the
`high-fidelity` / `wireframe` fidelity picker on the Live artifact
template) rendered as plain pill highlights, visually identical to
free-text and read-only text slots. The slot's `appearance: none`
strips the browser's default chevron, so users had no affordance
hinting the value was switchable.
This wraps select-type slots in an `inline-flex` span and overlays
an explicit chevron-down `Icon` against the slot's trailing padding
(bumped from 18px to 22px to make room). The chevron inherits the
accent color via the wrap's `color` property so it themes correctly
in light and dark modes. Click semantics are unchanged because the
chevron is `pointer-events: none`.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): size inline number slots to their value, not the whole row
Number-type inline slots in the home prompt (e.g. the slide-count
spinner on the Slide deck template) inherited the generic slot
`min-width: 8ch` and then stretched to the browser's default
`<input type=number>` width, so a two-digit value like "10" ate
the entire remaining line and pushed the native spinner buttons
to the far right edge — far away from the value they control.
This sizes the number input to its actual content plus four
characters of trailing room for the spinner buttons (clamped to
6–14ch), and adds a `home-hero__prompt-slot-input--number`
modifier that overrides the slot `min-width` to 4ch. The
spinner now sits flush against the value and the slot reads as
a compact inline pill matching the surrounding text/select
slots.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): widen prompt line-height so slot pills do not collide vertically
Each interactive slot and mention in the home prompt paints a 2px
outline ring via `box-shadow`. At the previous `line-height: 1.55`
on a 15px font, two lines were ~23px apart while a single pill
occupied ~20px of vertical space (text box + ring on both sides),
leaving roughly 2px of clear space between rows. When the prompt
wrapped onto multiple lines — common for the Image template's
"Generate a {kind} of {subject}. Style: {style}. Aspect: {ratio}."
example — the rings from line N and line N+1 visually merged into
a single bar, making it ambiguous which pill the user was about to
click or edit.
This bumps the line-height of both the highlight overlay and the
underlying editable textarea to 1.85 (~28px per line), restoring
~8px of clear space between pill rows. The two values must stay
identical so the overlay glyphs continue to track the textarea
caret positions exactly; a brief comment in each rule documents
this coupling for future edits.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): paint dropdown chevron on select element with neutral color
The previous chevron treatment wrapped each select slot in an
inline-flex span and laid an accent-orange `<Icon>` over the
trailing padding. At the prompt's normal display size the orange
chevron blended into the orange pill ring corners, so users
perceived "a bunch of dropdown arrows" across every highlighted
slot, even though only the actual select rendered one.
Move the chevron onto the `<select>` itself as a small neutral-
gray `background-image` SVG. The grey contrasts clearly with the
pill's accent ring and the chevron lives inside the select's own
padding, so it can never visually overlap the value text and can
never appear on non-select slots.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): shrink select-slot dropdown caret so it stops reading as a check-mark
The 8×6 stroked chevron looked like a check-mark in zoomed views
because the stroke width was a large fraction of the glyph. Swap
for a small (9×5) filled triangle drawn as a `background-image`,
with `background-size` pinned so browsers can't scale it to its
intrinsic SVG box. The caret is now unambiguously a dropdown
indicator without crowding the value text.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): make read-only prompt slots visually distinct from editable pills
Multi-word context values are rendered as plain <span>s with
pointer-events: none, but they inherited the same orange pill +
ring treatment as the truly editable <input>/<select> slots.
Users couldn't tell which pills they could click into.
Strip the pill background, the ring, the radius, and the padding
from `.home-hero__prompt-slot-text` and replace them with a
subtle dashed bottom border. The orange foreground keeps the
slot family link, but read-only highlights now clearly read as
"context value spliced into the prompt" rather than as
"interactive control".
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): scale select-slot dropdown caret up to 12px wide for legibility
The 9×5 caret was too quiet at the prompt's font size to read as
a dropdown affordance. Bump to a 12×6 filled triangle and pad the
select's trailing space (24px) so the value text still has clear
breathing room from the caret.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): collapse plugins search and filter strips into one bar
The plugins-home gallery laid out search, count, mode (Featured),
total-in-catalog, clear-filters, the main category row, and the
sub-category row as four floating clusters. Search lived up in
the section header, far from the chips it actually scopes; the
mode strip wedged "Featured + 386 in catalog + Clear filters"
between the header and the category row with no obvious
ownership; and the two clear-filter affordances duplicated each
other (chip strip + empty-state).
Fold the mode strip into the category row: Featured is now the
leading chip on the same line as the category pills, and the
search field, the result count, and a compact Clear link sit as
a right-aligned tools cluster on that same row. The header
shrinks back to just the title, subtitle, and Browse registry
link. The sub-category row stays as the contextual second line
when a category is active.
Tests: keeps the existing data-testids (plugins-home-chip-featured,
plugins-home-row-category, plugins-home-clear, plugins-home-count,
plugins-home-search) so the existing 11-case section spec passes
without modification.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): hide Recent projects rail on first-run home when empty
A brand-new user landing on Home saw an empty dashed box with the
copy "No projects yet — type a prompt to start one." sitting
above the plugin gallery. The hero already prompts the user to
type, so the empty rail just adds vertical noise without telling
them anything new.
Return null from RecentProjectsStrip when the recent list is
empty (loading or not) so the rail appears only when there is
actually something to show. Update the entry-chrome e2e to
assert toHaveCount(0) on first run — codifying the new contract
that the rail is conditional, not always-mounted.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): hide structured inputs form when every field is in the prompt
The PluginInputsForm below the chat textarea surfaced every plugin
input — even the ones the prompt template already substitutes
inline as highlighted slot pills. For a plugin like Prototype
that's five identical labelled inputs duplicating the five slot
pills above them, making the chat box look like it has grown a
second composer.
Compute the set of placeholder keys actually referenced in the
prompt template (via INPUT_PLACEHOLDER_PATTERN) and skip those
when deciding what the structured form needs to render. The form
still appears for plugins that expose inputs not referenced in
the prompt text (e.g. a "Run in background" toggle), but
template-only plugins collapse the redundant second editor.
Update the picker spec accordingly: when every field is in the
template the form is now expected to be absent rather than to
render a duplicate of the inline slot.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): drop redundant filtered-count and Clear link beside the search
The combined plugins filter bar trailed the search field with
"59 / 386" and an inline "Clear" link, but both repeat what the
chip strip already shows: every chip carries its own count
(Slides 59, All 386, …) and clicking the All chip already resets
the filters. Users had no way to know what the bare "59 / 386"
fraction or "Clear" referred to without inference.
Strip the trailing tools cluster down to just the search input.
The empty-results message at the bottom of the gallery still
exposes a contextual "Clear filters" button when a stacked
filter yields no matches, so the affordance isn't lost — just
removed from a position where it didn't read as actionable.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): drop default subtitle copy from the Official starters strip
The Home gallery rendered a two-line explanatory subtitle under
"Official starters" — "Ready-to-use Open Design workflows bundled
with this runtime. Pick one to load a starter prompt, or browse
the registry for more." — every visit. Returning users skip it,
new users get the same message from the section title plus the
Browse registry link plus the visual card grid itself; the prose
read as filler chrome above the chip strip.
Default the subtitle prop to undefined and only render the <p>
when a caller passes an explicit string. Other surfaces that
mount PluginsHomeSection with their own copy keep their
subtitle; the bare Home gallery loses one row of vertical noise.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fade out empty workflow lanes in Plugins home filter
Empty top-level lanes (Deploy 0, Refine 0, etc.) and empty
sub-categories (Vercel 0 under Deploy, Figma 0 under Import, etc.)
used to look identical to populated ones, so users couldn't tell at
a glance which chips were real catalog buckets and which were
"contribute a plugin" invites. The strip kept all lanes visible by
design — the workflow shape (Import / Create / Export / Share /
Deploy / Refine / Extend) is part of what we want users to see —
so we keep them clickable, but tag count-zero chips with
data-empty='true' and give them a faded, dashed-border treatment.
"All" pills stay solid since their count reflects the parent lane,
not their own emptiness.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): update home nav test expectations
* fix(e2e): align critical smoke with entry chrome
---------
Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(daemon): add Grok Build (xAI's first-party CLI) as agent runtime
xAI shipped Grok Build (https://x.ai/cli) on 2026-05-19 — a first-
party CLI agent that boxes their SuperGrok subscription into a
Claude-Code-shaped headless mode (`grok -p <prompt>`, optional
`--output-format streaming-json`, `--max-turns`, native subagent /
plan / mcp subcommands). The OAuth flow it ships uses the same
`auth.x.ai` issuer + `b1a00492-073a-47ea-816f-4c329264a828` client_id
+ `grok-cli:access api:access` scope that the existing OD xAI
Settings panel already speaks, so once a user runs the install
script and `grok login --oauth`, Grok Build is fully authenticated
without OD doing anything.
This runtime def lets OD detect a `grok` binary on PATH and surface
it in the agent picker the same way Hermes / Claude Code / Codex are
surfaced today. Headless invocations get:
- `grok -p` (prompt via stdin to avoid Windows ENAMETOOLONG / Linux
E2BIG on large composed prompts), with `--model` / `--effort`
pass-through when the user picks one in the picker.
- `grok models` for live model discovery (parsed line-by-line,
matching the existing `parseLineSeparatedModels` helper opencode /
cursor-agent already use). Falls back to a static list of
grok-build / grok-4.3 / grok-4.20-{reasoning,non-reasoning,
multi-agent} when probing fails (no Heavy entitlement, network
blip, etc.).
- `--effort` exposed as the standard reasoning-options dropdown.
streamFormat is intentionally `plain` for this first cut. Grok
Build's `streaming-json` output is a real NDJSON event stream, but
the schema is xAI-specific and OD does not yet have a parser for it
— shipping with `plain` now means the user can chat single-turn,
and a follow-up `grok-stream-json` adapter can light up tool_use /
plan-mode streaming once we've validated the shape against a live
Heavy account.
No test added for the def itself; the existing
`tests/runtimes/registry-and-args.test.ts` and `agent-args.test.ts`
iterate over `AGENT_DEFS` and now cover this entry by construction.
All 126 runtime tests pass; daemon tsc + tsconfig.tests.json clean;
pnpm guard clean.
* feat(web): add grok-build agent icon
Pairs with the new daemon runtime def in 35418fe so the Settings →
Agent Runtime row stops falling back to the "G" initial.
The SVG is a hand-drawn geometric "X" silhouette (Unicode 𝕏 / U+1D54F
shape), painted via AgentIcon's mono mask path so it picks up the
active theme color the way hermes / mimo / pi / kilo / cursor-agent
already do. xAI does not publish a downloadable brand SVG (favicon
paths on x.ai 404), so this is intentionally a placeholder rather
than a copy of any licensed mark — happy to swap for an official
asset if maintainers have one to hand.
Wires the id into both `ICON_EXT` and the `MONO_ICONS` set in
AgentIcon.tsx. No other behaviour changes.