README.ko.md is missing the "For desktop/background startup,
fixed-port restarts, and media generation dispatcher checks
(OD_BIN, OD_DAEMON_URL, apps/daemon/dist/cli.js), see
QUICKSTART.md" paragraph that the other 11 locale READMEs and
the English README all carry. Korean readers landing on the
README through the language switcher do not see this pointer
and have to discover the operational env vars by reading the
English QUICKSTART unprompted.
Add an equivalent paragraph in Korean at the same anchor
position the other 11 locales use — between the environment
requirements paragraph and the "첫 번째 로드 시:" enumeration.
The link target is `QUICKSTART.md` (English), matching how
ru/uk/es/tr/ar already cross-reference the English QUICKSTART
when no locale-specific QUICKSTART exists yet.
No code changes; one file, two lines added.
Co-authored-by: nicejames <nicejames@gmail.com>
Native macOS browser sometimes renders its own disclosure indicator even
with appearance:none, stacking it on top of the CSS background-image SVG
chevron. Apply the same orbit-template-select-wrap pattern: clear the
background-image via explicit wrapper class, use an absolutely-positioned
Icon component for a single, controlled chevron. Affects both the model
picker and reasoning picker selects in the agent-model-row.
* fix(security): resolve hostname before approving external API base URLs
Before this change the daemon-side base-URL validator only inspected the
literal hostname string. A public DNS record that points at an internal
address ('internal.example.com' → 10.0.0.5) passed validation, and the
daemon would issue the upstream request anyway — turning the BYOK proxy
into an SSRF primitive against internal infrastructure.
Add a small companion ('validateBaseUrlResolved') that runs the existing
sync check, resolves the hostname with 'dns.lookup({ all: true })', and
re-applies the block-list against every resolved address. Wire it into
the wrapper the daemon already uses ('validateExternalApiBaseUrl'), so
every proxy/finalize handler picks it up without further edits.
Carve-outs match the existing sync validator:
- Loopback hostnames skip DNS (Ollama-style local LLMs still work,
including '*.localhost' / 'lvh.me'-style names that resolve to 127.0.0.1
per RFC 6761).
- IP literals are already vetted by the sync pass; no need to re-resolve.
- DNS resolver errors fall through to the existing fetch error path —
a transient ENOTFOUND should not turn into a 403.
The 6 callers that previously consumed the sync result now 'await' the
async wrapper. All call sites are already inside async route handlers.
Vitest coverage in apps/daemon/tests/connection-test.test.ts covers:
sync rejection passthrough, loopback / IP-literal short-circuits,
private IPv4 and IPv6 resolution, dual-stack with one private record,
public→public passes (api.openai.com), '*.localhost' resolved→loopback,
and resolver-error fallback.
Detected by: aeon (manual review + semgrep + osv + trufflehog).
Class: CWE-918 (SSRF) — DNS-based bypass.
* fix(security): cover remaining daemon baseUrl fetch surfaces + Ollama redirects
Addresses PR #1176 review feedback (lefarcen / mrcfps): the resolved-IP
wrapper only covered the proxy/finalize routes, leaving three adjacent
SSRF gaps open.
- testProviderConnection (/api/test/connection provider mode): switch
from sync validateBaseUrl to await validateBaseUrlResolved so a
hostname that resolves to a private IP is rejected before the daemon
POSTs the smoke prompt upstream.
- listProviderModels (/api/provider/models): same swap. Import the
DNS-aware helper from ./connectionTest.js since it carries the dns
binding the daemon owns; contracts stays pure.
- Ollama proxy stream fetch: align with the other four proxy routes
by setting redirect: 'error', so a validated public host cannot 3xx
the daemon to a private/internal URL after the pre-fetch check.
Regression coverage:
- POST /api/provider/models — DNS spy returns 10.0.0.5 for a synthetic
hostname; route must respond { ok: false, kind: 'forbidden' } and
must not invoke upstream fetch.
- POST /api/test/connection provider mode — same shape.
- /api/proxy/ollama/stream — fetch mock asserts redirect: 'error' on
the upstream Ollama call.
The existing /api/provider/models timeout test now stubs dnsPromises
so it doesn't race the probe timer against real DNS.
---------
Co-authored-by: aeon <aeon@aaronjmars.com>
The .subtab-pill button hover rule hard-coded `rgba(255,255,255,0.6)` as the
hover background. In light theme this composites to a near-white wash that
sits cleanly under `--text`, but in dark theme it overlays a translucent
white on the dark panel and produces a bright, near-white surface beneath
the same light `--text`, dropping contrast to ~1.87 (well below WCAG AA 4.5).
The reporter (#1795, on 0.6.0) called this out on five Settings surfaces.
Four of them — BYOK / Appearance / Notifications / protocol-chip — were
incidentally fixed by the c5d77a03 dark-palette refresh because they use
the .seg-control / .seg-btn hover path, not the .subtab-pill path. The
Pet → pet source tabs (Built-in / Custom / Community) still reproduce on
0.7.0 because they use .subtab-pill, which never got a theme-aware hover.
Switch the hover background to `var(--bg-muted)`, an existing token that
mirrors `--bg-panel`'s active state shape (one step lighter than
`--bg-subtle`) and is defined per-theme. Composited contrast becomes
~9.4 in dark and ~14.3 in light against `--text`, comfortably above AA.
The same .subtab-pill rule is shared by RoutinesSection and DesignsTab, so
they pick up the fix as a side effect of fixing the source class.
Anchored by a Playwright regression test under e2e/ui/ that asserts WCAG
AA across both themes for the Pet source tabs and the seg-btn surfaces.
The test goes red on this commit's parent (Pets/dark, ratio 1.87) and
green here (all four assertions pass).
* docs(readme): add 0.8.0-preview banner pointing to Discussion #1727
Adds a single-line block-quote banner at the top of the English and
Simplified Chinese READMEs inviting visitors to try the 0.8.0 preview
and participate in shaping it.
The banner is intentionally short — all details (installer downloads,
contribution paths, Discord, scope, what we want feedback on) live on
the announcement thread at Discussion #1727 so the README stays
maintainable as a single source of truth.
Other language READMEs (es, pt-BR, de, fr, zh-TW, ko, ja-JP, ar, ru,
uk, tr) can adopt the same pattern via follow-up community PRs.
* docs(readme): punch up 0.8.0-preview banner to match announcement rally tone
Echo the signature lines from the announcement thread (Discussion #1727):
- "Design's old world ends here"
- "40k stars in two weeks got us this far"
- "We need you to push the rest of the way"
CTA link copy also reframed from generic "Discussion #1727" to action verbs:
"Read the announcement, grab the installer, join the movement" — matches
the rally-style framing of the announcement itself instead of reading
like a flat reference link.
* docs(readme.zh-CN): fix awkward Chinese phrasing for 40k stars line
"两周 40k stars 把我们送到这里" reads stilted in Chinese — the
number-as-subject construction is more English-like. Replace with the
phrasing already used in the announcement thread (Discussion #1727):
"上线两周,40k stars 在身,且仍在加速。剩下的路,需要你和我们一起推完。"
Maximum consistency with the Chinese announcement copy that's already
been reviewed and shipped.
* docs(readme): use GitHub Alert + H3 for higher-contrast 0.8.0 banner
A plain block-quote banner blended into the existing tagline and got
lost. Switch to GitHub's native '> [!IMPORTANT]' alert syntax — renders
as a purple-tinted callout card with an icon, which:
- Stands out clearly from the regular tagline that follows
- Matches the preview/v0.8.0 label color (#7c3aed purple)
- Renders consistently on github.com, mobile, and GitHub Desktop
- Needs zero images / inline CSS / external assets
Bumped the headline to H3 inside the alert for further visual weight,
broke the body into separate paragraphs for legibility.
* docs(readme): move 0.8.0-preview banner below language selector
After feedback, position the rally banner between the language selector
line and the '## Why this exists' heading instead of right under the H1.
Rationale: the very top of the README is for stable identity (title +
tagline + banner image + badges). The 0.8.0 alert sits in the
transitional position between identity-block and body-content, acting as
the last hook before readers commit to reading. Keeps the foundational
tagline visually pristine while still placing the call-to-action above
the fold.
* docs(readme.zh-CN): tweak CTA wording '拿安装包' → '下载安装包'
'下载安装包' is the natural Chinese verb pairing for installer downloads;
'拿' reads slightly casual / dialectal for a public-facing README banner.
* docs(readme): add 0.8.0-preview banner to all 11 remaining language variants
Translates the 0.8.0-preview rally banner into es, pt-BR, de, fr, zh-TW,
ko, ja-JP, ar, ru, uk, tr — placed in the same position (between the
language selector and the first horizontal rule) as the English and
zh-CN versions.
Each translation preserves the signature signals from Discussion #1727:
- 'Design's old world ends here' / its local-language equivalent
- '40k stars in two weeks' (number kept literal — same data point)
- 'We need you to push the rest of the way' / equivalent rally line
- 'agent-native' kept as English (no widely-accepted translation yet)
- 'Figma' kept as proper noun
Code blocks (`0.8.0-preview`), URLs, and the `[!IMPORTANT]` alert
syntax remain unchanged across all variants for renderer consistency.
If any native speaker spots a phrasing that reads less than fluent, a
follow-up PR is welcome — these translations are AI-assisted and should
be treated as starter copy, not final.
* docs(readme): move 0.8.0-preview banner back to top of all language READMEs
After more feedback, position the rally banner right after the H1 (`# Open Design`)
instead of below the language selector. Trade-off: the banner now competes with
the foundational tagline for above-the-fold attention, but visitors see the
0.8.0 call-to-action in their first eye-fix instead of after scrolling past
badges and language picker.
Same banner content, same alert syntax, same 13-language coverage — only
position changed across all variants (en, zh-CN, zh-TW, es, pt-BR, de, fr,
ko, ja-JP, ar, ru, uk, tr).
* docs(readme): add 'actively shipping / next phase / merges to main' line
Per colleague feedback, surface that preview/v0.8.0 isn't a fork or a
side experiment — it's the next phase of Open Design, currently in fast
development, set to land on main once stable.
The new paragraph sits between the 'we need you' rally line and the
final CTA link, so readers see the momentum context before clicking
through to the announcement.
Translated in place across all 13 language READMEs.
* docs(readme): update banner — 0.8.0 is already in main now
preview/v0.8.0 has been fully merged into main (0 commits behind, 59
commits behind main). The 'will merge into main once stable' line is
out of date.
Reframe to reflect current state:
- 'Actively shipping fast → set to merge into main once stable' (old)
- 'Already in main — 0.8.0 is the next phase, in active development.
Bring your contributions, ideas, and feedback.' (new)
Translated in place across all 13 language READMEs.
* docs(readme): drop awkward '合入 main' phrasing from banner
The 'merged into main' / '已经合入 main' line reads too git-operational
for a public banner. Reframe without referencing merge mechanics:
old: 'Already in main — 0.8.0 is the next phase, in active dev. Bring
your contributions, ideas, and feedback.'
new: 'Active development, open to all — 0.8.0 is the next phase of
Open Design. Your contributions, ideas, and feedback shape what
comes next.'
Same intent — current direction, welcoming, no version-control
vocabulary that non-engineer visitors won't connect to.
Translated in place across all 13 language READMEs.
* docs(readme): tighten banner phrasing — 'iterating fast on main'
Replace 'Active development, open to all' with a more dynamic line that
brings 'main' back into the copy without the static 'merged into main'
feel:
'**Iterating fast on `main`** — 0.8.0 is the next phase of Open Design.
Your contributions, ideas, and feedback shape what comes next.'
Conveys ongoing forward motion (vs the completed-merge frame), keeps
the 'main' anchor so readers know where to look. Translated across all
13 language READMEs.
* docs(readme): amp up banner CTA — concrete actions + 'what you bring is what this movement becomes'
The previous 'Your contributions, ideas, and feedback shape what comes
next' was directionally right but soft. Swap in three concrete imperative
verbs (PR / idea / bug) plus a collective-ownership punchline:
'**Iterating fast on `main`** — 0.8.0 is the next phase of Open Design.
Ship a PR, drop a wild idea, file a bug — what you bring is what this
movement becomes.'
Reads like an invitation to act, not a thank-you-in-advance. Translated
across all 13 language READMEs.
* docs(readme): mention Claude Design alongside Figma in banner
Update paragraph 2 from 'alternative to Figma' to 'alternative to Claude
Design / Figma' across all 13 language READMEs.
Aligns the banner with the foundational tagline below it (which already
points to [Claude Design][cd]). The framing is now: Open Design competes
with both closed silos — Anthropic's Claude Design *and* Figma — by
being the open, agent-native answer to the same opportunity.
* docs: point 0.8.0 preview contributors at main, not preview/v0.8.0
0.8.0 has been merged into main (#1832). Anywhere we used to tell
contributors to checkout / PR against preview/v0.8.0 was actively
mis-routing new PRs. Update:
- docs/preview-v0.8.0-announcement.md + zh-CN: status line, Branch row,
source-build checkout, and 'open a PR against' guidance now point at
main
- .github/ISSUE_TEMPLATE/bug-report.yml + feature-request.yml: phrase
the 'use the preview template' nudge as 'about the 0.8.0 preview
features (now on main)' instead of 'about the preview/v0.8.0 branch'
- .github/ISSUE_TEMPLATE/config.yml: same rewording for the contact link
- .github/ISSUE_TEMPLATE/preview-v0.8.0-feedback.yml: refresh the
description and the intro body so it reads as 'preview features
pre-tag', not 'features pre-merge'
The preview-v0.8.0-feedback template and preview/v0.8.0 label are
intentionally kept: 0.8.0 isn't tagged yet, so we still want a
dedicated lane for preview-features feedback.
* chore: stop treating preview/v0.8.0 as a live branch
Earlier in this PR we kept the preview-v0.8.0 surface area intact —
that was the wrong call. 0.8.0 is now on main; pretending there's a
parallel 'preview' branch in the templates, labels, and copy was going
to keep mis-routing contributors.
Drop:
- .github/ISSUE_TEMPLATE/preview-v0.8.0-feedback.yml (the dedicated
template that auto-applied the preview/v0.8.0 label and prefix)
- .github/ISSUE_TEMPLATE/config.yml contact_links entry pointing at it
- bug-report.yml + feature-request.yml nudges that sent users there
- The Preview-v0.8.0-feedback link block from both announcement docs
(replaced with normal bug-report / feature-request links)
Rename:
- docs/preview-v0.8.0-announcement.{md,zh-CN.md}
-> docs/v0.8.0-announcement.{md,zh-CN.md}
so the on-disk doc title reads as a 0.8.0 announcement, not a
branch-specific one. No other repo file referenced the old paths.
The preview/v0.8.0 label and branch themselves are intentionally
untouched — those are separate ops the maintainer will decide on
later. This PR only removes mentions inside the repo.
* chore: keep 0.8.0 preview-feedback template as a chooser-level ad
The previous commit deleted preview-v0.8.0-feedback.yml entirely. Bring
it back, but reframe it: it's now the dedicated 0.8.0 lane in the
issue chooser — a high-visibility surface that tells visitors "0.8.0
is here as a preview, please share what you noticed."
- Renamed in the chooser to "Open Design 0.8.0 — preview feedback"
- Title prefix shortened from "[preview/v0.8.0] " to "[0.8.0] " so the
branch slug no longer leaks into issue titles
- label preview/v0.8.0 still auto-applied (the label entity is still in
use across 26 issues; maintainer will decide on its fate separately)
- Area dropdown widened from "Skills + Automations" to cover the
actual 0.8.0 surface (plugins, headless, agent flow, desktop shell)
- Intro body rewritten to read as a preview-release ad, not a
feature-branch tester request
Announcement docs (English + Chinese) also routed their "open an
issue" CTA back through this template instead of the generic bug-report
/ feature-request links — same advertising goal.
The packaged Linux headless entry crashed at module-load with
ERR_MODULE_NOT_FOUND for 'electron'. Trace from the CI artifact:
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'electron' imported
from .../packaged/dist/headless.mjs
at Object.getPackageJSONURL ...
Root cause: headless.ts pulls in `./config.js` for the
PACKAGED_NAMESPACE_ENV constant and the PackagedConfig type. config.ts
had a top-level `import { app } from "electron"`, so ESM had to resolve
electron at module load even though headless never calls the two
functions (readRawPackagedConfig / readPackagedConfig) that actually use
`app`. Headless deliberately ships without electron in its
node_modules, so resolution failed.
Move the electron `app` import behind a `loadElectronApp()` helper that
calls `await import("electron")` only when one of the two electron-only
functions runs. headless.ts can now load config.ts to read constants
and types without ever pulling electron in.
Currently the artifact only contains vitest.log + the tools-pack build
log. The packaged daemon's own stdout/stderr lands in
$RUNNER_TEMP/tools-pack/runtime/linux/namespaces/<ns>/logs/desktop/latest.log,
outside the artifact path, so when 'headless-root.json not written
within 35s' fires we have no signal on what the spawned child actually
did.
Copy the runtime logs/ and runtime/ subdirectories into the report dir
before upload so the artifact captures daemon/web sidecar startup
output, identity markers, and per-app state JSON.
The v0.8.0 launch doc explicitly lists Linux as "coming soon" — the
packaged Linux headless runtime is known-incomplete in this release.
Keep the smoke job running so we keep collecting signal, but don't block
PRs on it until the Linux client lands.
Re-enable (drop continue-on-error) once the Linux packaged client is
ready in a future release.
The merge brought new entries into pnpm-lock.yaml (Leonardo.ai provider,
custom select primitive, Critique Theater wireup, etc.), so the vendored
pnpm store hash changed. CI computed the new hash from the freshly built
fixed-output derivation.
specified: sha256-mzGiD5K1l5SEFpQ++L4wq775ne+ViG4ruXYZYEdT6zQ= (pre-merge)
got: sha256-lROdH5HgKFf3R7DYGbc8n/GrmINwLbfVwC4Xp7SrHN4= (post-merge)
Pulls in the teammate's PR #1830 — e2e align to redesigned entry flows
+ watcher and codex runtime test fixes. Conflicts resolved:
- apps/daemon/tests/runtimes/registry-and-args.test.ts: imported both
withPlatform (HEAD, used 6x) and withEnvSnapshot (theirs, used 1x);
both helpers are needed in the file.
- apps/web/src/components/ProjectView.tsx: kept both ChatPane-remount
guards. Main's fix#1710 uses lastSyncedConversationIdRef tracking URL
sync; the teammate added lastSeenRouteConversationIdRef tracking last
observed route. Both refs are already defined in the file and the two
checks are complementary defenses.
- e2e/ui/settings-*.test.ts (5 files): took teammate's version wholesale
— they replaced inline `page.goto('/') + click Execution mode` with
`gotoEntryHome + openSettingsDialogFromEntry` helpers, which is exactly
the PR's intent.
The merge of origin/main into preview/v0.8.0 left two typecheck regressions:
- onImportFolder signature: main (#1666) widened the return to
boolean | Promise<boolean> so the caller can surface an error toast
on failed manual folder paths. NewProjectModal and EntryShell still
carried the older void variant, breaking the spread into NewProjectPanel.
Align both to boolean | Promise<boolean>.
- Italian locale (#1323) was authored against an older Dict that still
had pet.rail* keys. v0.8.0 has since dropped the pet rail UI, so the
it.ts entries fail strict object-literal checking. Remove pet.railTitle,
pet.railHint, pet.railExpand, pet.railCollapse, pet.railHide,
pet.railShow, pet.railCustomFlavor, pet.railCustomize.
* feat(landing): add blog indexing automation
Automate supported blog discovery checks through sitemap submission, URL Inspection monitoring, IndexNow notifications, and guarded SEO CI checks.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(landing): support oauth for blog indexing
Use OAuth refresh-token auth as the preferred Search Console path while keeping service-account auth as a fallback, so the indexing workflows can run despite GSC service-account invite issues.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(landing): tighten blog indexing observability
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(design-systems): add structured tokens for notion, linear, github, figma, slack, discord, openai, shopify, spotify, uber
Hand-authored batch-2 oracle fixtures for 10 high-profile brands.
Each brand ships tokens.css (A1/A2/B-slot declarations) + components.html
(:root pasted verbatim, no off-token values). pnpm guard reports 12
structured brands (up from 2), all 13 checks pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(design-systems): correct elev-ring, focus-ring, and spacing schema drift
- slack: rebind --elev-ring to hairline form (0 0 0 1px var(--border)) so
cross-brand components expecting a 1px outline get a ring, not a soft
drop shadow; the previous value was Slack DESIGN.md "Low" elevation
accidentally placed in the wrong slot
- linear-app: expand --focus-ring to the DESIGN.md §6/§7 multi-layer
stack (accent ring + black blur) so keyboard focus is visible on the
near-black #08090a canvas
- openai: restore --space-{5,6,8,12} to the shared 4·N px spine and
introduce --space-16 (64 px) as a brand extension in BRAND_EXTENSIONS;
syncs components.html :root in all three brands
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: add Leonardo.ai image provider integration
Implements Leonardo.ai as a fully supported image provider with the following models:
- Phoenix (leonardo-phoenix) - versatile general-purpose model
- Kino XL (leonardo-kino-xl) - cinematic quality
- FLUX Dev (leonardo-flux-dev) - FLUX.1 [dev]
- FLUX Schnell (leonardo-flux-schnell) - fast generation
- Anime Pastel Dream (leonardo-anime-pastel) - anime style
Features:
- Async generation with polling (2-minute timeout)
- Support for standard aspect ratios: 1:1, 16:9, 9:16, 4:3, 3:4
- Bearer token authentication
- Automatic image format detection
Implementation:
- Frontend: apps/web/src/media/models.ts
- Backend: apps/daemon/src/media-models.ts
- Renderer: renderLeonardoImage() function (~130 lines)
- Dispatcher: integrated into media generation pipeline
API Integration:
- Submit: POST /generations
- Poll: GET /generations/{id}
- Response: generations_by_pk.generated_images[0].url
Addresses #984
* fix: Add leonardo to MediaProviderId type and register env vars
1. Add 'leonardo' to MediaProviderId union in apps/web/src/media/models.ts
to fix TypeScript build error
2. Register LEONARDO_API_KEY env vars in apps/daemon/src/media-config.ts
following the same pattern as other providers
This fixes the CI TypeScript build failure and enables proper env-based
credential lookup for Leonardo.ai provider.
---------
Co-authored-by: lefarcen <935902669@qq.com>
* fix(landing): unify blog chrome and stars
Ensure blog and catalog pages share the same header/footer behavior, with safe GitHub star fallbacks and RSS discovery for the refreshed blog.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(nix): update pnpm dependency hash
Keep Nix fixed-output dependency hashes aligned with the landing page lockfile changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(design-systems): add structured tokens for cursor brand
Introduces design-systems/cursor/tokens.css + components.html, the
third structured brand after default and kami. Brings the prose
DESIGN.md into the schema-checked form so agents picking the cursor
design system get the structured token channel (PR-D #1544) instead
of having to re-derive Cursor's warm-cream palette, three-voice
typography (CursorGothic / jjannon / berkeleyMono), oklab borders,
and depth-based focus from prose every turn.
Why now: PR-D made the structured channel default-on, but it only
materially helps the 2 brands shipping tokens.css today. To expand
batch-design-system-test (#1515) coverage beyond default + kami,
we are hand-authoring 5 high-profile brands (cursor first as
template, then apple / stripe / airbnb / vercel) before the bulk
derive script (PR-B). These five also serve as the byte-identical
oracle the future derive script must reproduce.
Brand-specific schema decisions, captured in the tokens.css header:
- Surface: 3-tier cream ladder (#f2f1ed → #ebeae5 → #e6e5e0).
--surface-warm binds to a real intermediate tier, not aliased.
- Foreground: 4-tier ramp via rgba(38,37,30,a) at 1.0 / 0.9 / 0.55
/ 0.4 — Cursor genuinely has the richer ramp.
- Borders: oklab origins shipped as rgba fallbacks per DESIGN.md
§Agent Prompt Guide rule 3.
- --tracking-display: -0.03em (the 72px hero value normalized to
em). Smaller display sizes scale tracking proportionally inline.
- --elev-raised: signature 28/70px diffused atmospheric shadow.
- --focus-ring: depth-only (0 4px 12px), never the cool-blue halo.
- --ease-standard: ease (per DESIGN.md), not the schema cubic-bezier.
- --danger: warm crimson #cf2d56, used for the brand-signature
hover-text-shift on buttons.
components.html demonstrates the full surface (hero with 72px
CursorGothic + jjannon serif lead, three-voice typography, warm-
surface CTA with crimson hover, accent-orange pill CTA used once,
oklab-fallback bordered cards, AI timeline signature element with
the warm pastel state colors, jjannon-serif input with 4-12 depth
focus). :root paste is byte-identical to tokens.css.
Verified:
- pnpm guard — 13/13 checks pass, including all 8 design-system
sub-checks now reporting "3 brands" (default + kami + cursor):
- token-fixture sync: 3 brand pairs aligned
- A1/A2/B-slot required tokens: all 3 brands declare
26 A1 + 26 A2 + 4 B-slot
- unknown token allowlist: 185 declarations across 3 brands all
match shared schema or brand extensions
- flag parity: 146 prose-only brands byte-identical, 3 structured
diverge as expected
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(design-systems): correct h2/h3 line-height in cursor components fixture
Per DESIGN.md §Hierarchy, 36px section headings use line-height 1.20
and 26px sub-headings use 1.25. The shared h1/h2/h3 rule was locking
all three to --leading-tight (1.10), baking wrong vertical rhythm into
the oracle fixture.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(design-systems): add structured tokens for apple, stripe, airbnb, vercel brands
Batch follow-up to #1652 (cursor). Adds tokens.css + components.html
for the remaining four high-profile brands, using cursor as the
format/template oracle.
Each brand encodes its specific identity:
- apple: SF Pro tight-compressed display (--leading-tight 1.05),
system-blue accent, SF Symbols grid, frosted glass surfaces
- stripe: sohne-var weight-300 anti-convention signature, --leading-tight
1.10 (matches all heading spec values exactly), indigo accent
- airbnb: Cereal weight-700 display with Rausch coral, --leading-tight
1.20 with per-size overrides (h2→1.18, h3→1.25)
- vercel: Geist 600 compressed display, --leading-tight 1.10 with
per-size overrides (h2→1.20, h3→1.33 per DESIGN.md §Typography)
pnpm guard: 13/13 checks pass; flag-parity now shows 6 structured
brands and 143 prose-only brands.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: add Italian (it) locale support
- Add complete Italian translation dictionary (apps/web/src/i18n/locales/it.ts)
- Register 'it' locale in types.ts (Locale type, LOCALES array, LOCALE_LABEL)
- Import and register Italian dictionary in index.tsx
- All 1352 translation keys translated to Italian
- Follows the same structure as French locale (PR #326)
Closes#1245
* test: Update locale tests to include Italian (it)
1. Add 'it' to EXPECTED_LOCALES array
2. Add LOCALE_LABEL.it assertion to verify 'Italiano' label
This fixes the CI test failure and completes the Italian locale integration.
---------
Co-authored-by: lefarcen <935902669@qq.com>
* docs: plan linux client issue 709
* fix: complete linux headless lifecycle routing
* feat: add linux packaged inspect
* test: add linux headless packaged smoke
* ci: add linux headless packaged smoke
* ci: smoke linux AppImage release artifacts
* docs: document linux packaged client status
* chore: finalize linux client audit remediation
* docs: add linux client publication packet
* test: harden linux client smoke coverage
* ci: preserve linux smoke audit evidence
* refactor: consolidate linux e2e helpers
Move pathExists and the desktop/web/daemon app-key array out of
linux.spec.ts into linux-helpers.ts, where expectPathInside and
linuxUserHome already live. Keeps the spec file focused on tests and
the helpers file as the canonical home for shared Linux e2e utilities.
* fix: move linux e2e helpers to lib
* fix: address linux release review blockers
* fix: drop npm dependency from containerized linux build
writeAssembledApp() previously called runNpmInstall() which executed
`npm install` directly. Inside the containerized build path,
electronuserland/builder:base strips npm/npx/corepack, so the inner
tools-pack build would fail at the assembled-app install step.
Route the install through OD_TOOLS_PACK_PNPM_BIN: buildDockerArgs sets
the env to the standalone pnpm binary it bootstraps, and the new
resolveProductionInstallCommand helper consumes that env to run
`<bin> install --prod --no-lockfile --config.node-linker=hoisted`.
Host invocations with no env set keep the prior npm behavior.
--config.node-linker=hoisted preserves the flat node_modules layout
that electron-builder packs the same way as npm-installed trees.
New tests cover the resolver branches and assert the docker-arg-to-
resolver chain end-to-end so reviewers can see the container's inner
build receives the env that switches its install away from npm.
* fix: harden linux container bootstrap
* fix: validate desktop marker liveness in headless cleanup
cleanup --headless previously skipped on any parseable desktop-root.json, trapping recovery when the AppImage had crashed and left a stale marker. Validate the marker the same way stopPackedLinuxApp does: if the PID is not in the live snapshot list, proceed through cleanup instead of skipping.
Extract the validation into validateDesktopAppImageMarker so the stop and cleanup paths share one definition of live and owned. Tests cover both branches: a stale marker drives cleanup to remove the runtime/output roots, while a live marker drives cleanup to skip and preserve them.
* 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.
* Prevent imported Claude canvases from zooming on scroll
Claude Design exports can classify ordinary macOS two-finger vertical wheel events as mouse-wheel zoom clicks inside design-canvas.jsx. Normalize that imported canvas code so plain wheel input pans, while Cmd+wheel remains the explicit zoom gesture.
Constraint: The offending canvas code lives inside imported user artifacts rather than a tracked runtime component, so the fix belongs in the Claude Design zip import normalization path.\nRejected: Host-side wheel interception | wheel events inside the sandboxed iframe are handled by the artifact before the host can reliably classify them.\nRejected: Disable all wheel zoom | users still need Cmd+wheel as an explicit zoom control.\nConfidence: high\nScope-risk: narrow\nDirective: Keep plain wheel as pan-only for imported design-canvas.jsx unless a future bridge provides an explicit wheel-mode handshake.\nTested: pnpm --filter @open-design/daemon exec vitest run tests/claude-design-import.test.ts\nTested: pnpm --filter @open-design/daemon typecheck\nTested: pnpm guard
---------
Co-authored-by: nicejames <nicejames@gmail.com>
Co-authored-by: lefarcen <935902669@qq.com>
* fix: Add error feedback for manual folder path import
Fixes#1408
When users manually enter a folder path and click 'Open folder' (non-Electron
environment), the app now provides clear error feedback if the import fails.
**Before:**
- No error clearing before import
- No error handling for failed imports
- Silent failures left users confused
**After:**
- Clears previous errors before attempting import
- Catches and displays import errors with clear messages
- Success feedback is implicit (navigation to the opened project)
**Why implicit success feedback:**
The parent handler (Home.tsx) navigates to the newly opened project on success,
which provides clear visual feedback by changing the entire view. An additional
toast would be redundant.
**Error handling:**
- Catches all errors from onImportFolder
- Displays user-friendly error messages
- Preserves error details when available
* fix: surface failed folder imports
---------
Co-authored-by: Siri-Ray <2667192167@qq.com>
Add sticky positioning to .tab-panel-toolbar to keep filter controls
visible while browsing the template gallery. This matches the sticky
behavior of other headers in the app (chat header, kanban columns, etc.)
and improves the browsing experience.
Changes:
- Added position: sticky, top: 0, z-index: 4
- Added background: var(--bg-panel) to prevent content showing through
Fixes#1778
- Add i18n keys for comment panel UI strings under chat.comments.*
- Replace hardcoded English strings in FileViewer internal components
(BoardComposerPopover, CommentSidePanel, formatCommentTime,
commentDisplayLabel) with t() calls
- Rename "Send to Claude" button to "Send to chat" (#1390)
- Add translations for en, zh-CN, de; English fallbacks for other locales
- Add common.weeksAgo for time formatting
Fixes#1392Fixes#1390
isLocalSameOrigin's no-Origin branch hard-coded the extras list to [],
so a POST from a configured LAN/Tailscale host (e.g. http://100.86.154.169:7456)
with no Origin header was rejected even though the operator allow-listed it.
Filter extraAllowedOrigins to entries whose hostname is an IP literal
(IPv4 dotted-quad or bracketed IPv6) and pass that subset to the
Host-only isAllowedBrowserHost call. Named-domain entries still require
an Origin header on guarded routes, preserving the existing CSRF invariant
(covered by the pre-existing 'rejects no-Origin POST when Host matches a
non-loopback configured origin domain' test).
Fixes#1337
References: This PR takes a different approach from #1343, which was identified as introducing a CSRF security regression (CHANGES_REQUESTED by reviewers lefarcen and Siri-Ray): simply passing extraAllowedOrigins to the Host-only branch allows no-Origin requests from configured deployment hosts to bypass CSRF guards. This fix addresses the root cause with a safer approach that preserves the Host-only/no-Origin rejection invariant while enabling correct OD_ALLOWED_ORIGINS behavior for requests that include an Origin header.
BYOK/API-mode chats bypass the daemon run path, so attached project
files were saved as message metadata but their readable contents were
not sent to the provider. This adds a web-side attachment context step
for API-mode requests, reusing raw text reads and existing document
preview extraction.
Constraint: Docker PDF previews require pdftotext in the runtime image
Confidence: high
Scope-risk: moderate
Tested: corepack pnpm --filter @open-design/web test -- tests/api-attachment-context.test.ts tests/components/ProjectView.api-empty-response.test.tsx
Tested: corepack pnpm --filter @open-design/web typecheck
Tested: corepack pnpm --filter @open-design/web build
Tested: corepack pnpm guard
Tested: corepack pnpm typecheck
* feat(claude): wire AskUserQuestion tool through chat + pin TodoWrite
Claude calls `AskUserQuestion` for mid-conversation clarifications when
the natural answer is one of a small finite set of choices. Until now
the tool round trip hit two dead ends in headless mode: claude-code -p
cannot prompt the user, so it auto-errored the tool and retried 4x;
the model then hedged by also writing the same options as a markdown
bulleted list. The host had no way to feed a real `tool_result` back.
This change makes the AskUserQuestion round trip work end to end:
* Switch Claude to `--input-format stream-json`. The daemon wraps the
prompt as a JSONL `user` message on stdin and keeps stdin OPEN, so
later writes (a `tool_result` for the open AskUserQuestion) feed
back into the same child instead of needing a fresh spawn.
* New `RuntimeAdapter.promptInputFormat()` ('text' default,
'stream-json' for Claude) so the spawn loop keeps the old close-on-
prompt behavior for every other agent.
* New `POST /api/runs/:id/tool-result` daemon endpoint and
`submitChatRunToolResult` web helper. Body carries `toolUseId` and
`content`; daemon writes a JSONL `user` message with the matching
`tool_result` content block.
* Track outstanding host answers on the run (`pendingHostAnswers`)
and close stdin on either a `usage` event or a synthesized
`turn_end` event (extracted from `assistant.message.stop_reason`
in `claude-stream`). Without the per-turn `turn_end` signal stdin
would never close after the follow up turn finished and the run
would hang until the inactivity watchdog killed it.
* System prompt: tell Claude to use AskUserQuestion for follow ups
with 2-4 finite choices, and to STOP after the tool call instead
of writing a markdown duplicate.
Web UI:
* New `AskUserQuestionCard` renders the tool input as labelled chip
buttons (single or multi select) with a Submit button styled like
the composer's Send. On submit the answer routes through
`submitChatRunToolResult` (live tool_result path) and falls back
to `onSubmitForm` (plain user message) only if the run has already
terminated. Selected chips persist across page reloads by re
parsing the stored `tool_result.content`.
* Hide markdown text that follows an AskUserQuestion in the same
turn — defense in depth against the model emitting the duplicate.
* Collapse identical `AskUserQuestion` / `TodoWrite` retries inside
any tool group to a single card. TodoWrite is a snapshot tool,
so older calls are duplicates of state.
* Pinned TodoCard above the chat composer. The latest TodoWrite
snapshot across the conversation renders once, expandable /
collapsible header, count shows in-progress + completed (1/4),
Done button dismisses when all tasks finish, soft fade gradient
above so scrolling chat text dissolves into the panel instead of
hard clipping under the card.
* Composer gains a top shadow that only appears when the pinned
todo slot sits directly above it (dark mode strengthened).
* Accordion expand / collapse motion shared between TodoCard, the
ToolGroupCard disclosure, and BashCard output via
`grid-template-rows: 0fr -> 1fr` with `cubic-bezier(0.23, 1, 0.32, 1)`
and asymmetric durations (200ms enter, 140ms exit) per Emil
Kowalski's animation framework.
* Jump-to-latest button no longer unmounts on hide; slides up with
scale 0.9 -> 1 + fade on show, slides down with scale + fade on
hide. Always horizontally centered via `margin: 0 auto`.
i18n:
* `tool.askQuestion`, `tool.askQuestionSubmit`, `tool.askQuestionPending`,
`tool.askQuestionAnswered`, `tool.todosExpand`, `tool.todosCollapse`,
`tool.todosDone`, `tool.todosDismiss` added to all 18 locales.
Unblocker:
* Fix a pre-existing render loop in `ProjectView` when the user
clicks "New conversation". `handleNewConversation` now navigates
to the fresh conversation id synchronously after
`setActiveConversationId` so the route-sync effect at L512 and
the URL-sync effect at L851 do not ping pong (route mismatch
triggered repeated reverts; React's nested-update guard fired).
* fix(claude): order turn_end after content blocks + cover chat switching
Two follow-up fixes to the AskUserQuestion + new-conversation work:
* `claude-stream.ts` emitted `turn_end` BEFORE iterating the assistant
message's content blocks. When claude-code lacks
`--include-partial-messages` (older builds), tool_use events surface
only from that loop, so the daemon's stdin-close handler saw an
empty `pendingHostAnswers` set and closed stdin before the
AskUserQuestion tool_use was even registered. The result: the model
retried, hit the same race, and gave up writing the questions in
prose. Emit `turn_end` AFTER the content loop so tool_use ids land
in `pendingHostAnswers` first.
* `server.ts` now ignores `turn_end` events with
`stop_reason: 'tool_use'`. That stop reason means the model paused
to wait for a tool execution (claude-code's internal tool runner
for Bash / Edit / Read, or a host-answered tool like
AskUserQuestion). Either way the conversation is still in flight —
closing stdin there would kill the follow-up response. Only the
natural turn-end stop reasons (`end_turn`, etc.) close stdin.
* `ProjectView.handleSelectConversation` now navigates to the picked
conversation id synchronously, mirroring the fix already in
handleNewConversation. The route-sync effect at L512 was reverting
the active conversation on every switch, ping-ponging with the
URL-sync effect at L851 until React's nested-update guard fired
with "Maximum update depth exceeded". Same bug class as the
pre-existing new-conversation render loop.
* docs(agents): capture AskUserQuestion runtime + chat UI conventions
Record the patterns this PR introduces so future contributors can find
them without spelunking server.ts:
* Agent runtime conventions — `RuntimeAgentDef.promptInputFormat`,
`run.pendingHostAnswers` / `run.stdinOpen` lifecycle, `turn_end`
ordering rule, `POST /api/runs/:id/tool-result` endpoint shape, the
Claude only system prompt block that nudges AskUserQuestion, and the
`suppressAskUserQuestionFallbackText` defense in depth.
* Chat UI conventions — URL-load vs srcDoc render mode dispatch with
bridge disqualifiers, the dual iframe visibility swap pattern,
`isOurIframe` plus the active-iframe re-check for signals that must
only come from the visible iframe, pinned TodoCard via
`PinnedTodoSlot`, count includes `in_progress`,
`dedupeSnapshotToolRetries` for AskUserQuestion / TodoWrite stacks.
* i18n keys — 18 locale files, add the key to `types.ts` first.
* UI animation philosophy — `cubic-bezier(0.23, 1, 0.32, 1)` ease out,
asymmetric 200/140ms enter/exit, accordion via `grid-template-rows`,
no `transform: scale(0)`, keep mounted + toggle class for exit
transitions instead of relying on React unmount.
* fix(claude): read promptInputFormat as field, close stdin on deferred answer
Two PR review follow-ups on the AskUserQuestion stream-json wiring.
* server.ts:4616 referenced `runtimeAdapter.promptInputFormat()` — but
`runtimeAdapter` is not declared, imported, or assigned anywhere. The
prior adapter abstraction was deleted in #1656; when the changes
were folded back into the inline handler the format was moved onto
`RuntimeAgentDef.promptInputFormat`, but this call site was missed.
`server.ts` starts with `// @ts-nocheck` so typecheck never caught
it — every chat run hit `ReferenceError: runtimeAdapter is not
defined` the moment we wrote the prompt to a stdin-fed child, which
is every agent with `promptViaStdin: true` (claude, codex, copilot,
cursor-agent, gemini, opencode, pi, qoder). Read the format off the
in-scope `def` and default missing values to `'text'`.
* `submitToolResultToRun` cleared the answered id from
`pendingHostAnswers` but never closed stdin if a `turn_end` /
`usage` event had already fired with the set non-empty (deferred
by the event handler). The child then waited indefinitely for
further input until the inactivity watchdog killed it, losing the
model's follow-up response. Close stdin on the last-answer
transition when stream-json stdin is still open.
Test: pin `promptInputFormat` for every `promptViaStdin: true` agent
so future regressions of the field-vs-method contract fail at
typecheck-adjacent test time instead of in production. The new test
asserts `typeof def.promptInputFormat` is a string (or undefined),
not a function — exactly the shape mistake the original line made.
* fix(web): keep AskUserQuestion multi-select chips selected after reload when labels contain commas
`handleSubmit` joined multi-select answers with `', '` while the
reload parser split them on `','`. The pair is asymmetric: a valid
model-generated option like `"Yes, including images"` round-tripped
as `["Yes", "including images"]`, so after a page reload the locked
question card showed the user's pick as unselected — even though the
`tool_result` content the daemon actually wrote into the run was
correct, and the model saw the right answer. Bounded to post-reload
visual state, but silently confusing.
Switch to a `- ` bullet list per option, one per line, with the
parser stripping the leading `- ` back off. Newlines never appear
inside a label so the round trip is exact. The outer pairs separator
stays `\n\n` because individual answer bodies still never contain
that double-newline.
* chore: drop accidental personal design-system file
`design-systems/foldar/DESIGN.md` was added to the AskUserQuestion
branch in 31ac531 by mistake — it's a personal brand spec that does
not belong in the upstream design-systems catalogue. Removing it
keeps the branch's surface area scoped to the feature.
* fix(daemon): raise ACP stage timeout to match outer chat inactivity watchdog
The ACP per-stage silence watchdog defaulted to 180_000ms (3 min), which
killed long generations mid-response (e.g. an agent silently writing a
large HTML artifact). The outer chat-run inactivity watchdog is already
600_000ms (10 min) precisely because 'agents [...] spend several minutes
silently writing large artifacts' (server.ts), so the inner ACP watchdog
was undercutting that design intent with no env override.
- Raise DEFAULT_STAGE_TIMEOUT_MS from 180_000 to 600_000.
- Add OD_ACP_STAGE_TIMEOUT_MS env override resolved in server.ts and
threaded into attachAcpSession via stageTimeoutMs.
- Bound the override by MAX_CHAT_RUN_INACTIVITY_TIMEOUT_MS so oversized
values do not get clamped to 1ms by Node's signed-32-bit delay limit.
- Add red-spec tests covering the new default and the override path.
* fix(daemon): treat OD_ACP_STAGE_TIMEOUT_MS<=0 as disable, not 0ms schedule
Addresses review feedback from @Siri-Ray on #1734.
The resolveAcpStageTimeoutMs() clamp passes 0 (or any negative value
normalized to 0) straight through to attachAcpSession, which would then
call setTimeout(..., 0) and fail every ACP session on the next tick.
The outer chat inactivity watchdog at server.ts already treats <=0 as a
disable escape hatch (see inactivityTimer); the inner stage watchdog now
mirrors that semantic.
- attachAcpSession now skips resetStageTimer scheduling when the
effective stageTimeoutMs is <=0.
- Added regression test locking in the disable semantics: a 60-minute
silent period under stageTimeoutMs=0 emits no error and does not kill
the child.
* feat(landing): editorial blog UI + Blog nav entry
Adds the magazine-style /blog/ index and post detail pages backed by an
Astro content collection of long-form posts (Product, Guides, Use cases,
Community), and threads a Blog entry through the shared site Header so
blog readers see the same Skills/Systems/Templates/Craft nav as the
home page.
What's in:
- header.tsx: add Blog item to nav-links + 'blog' active highlight key
- pages/blog/index.astro: editorial list with featured card, category
filter chips, and shared <Header counts={...} active="blog" />
- pages/blog/[slug].astro: long-form post template with SeoHead article
JSON-LD, post-topline kicker (← Back to Blog + category · date),
and 'View source on GitHub ↗' footer link
- _components/seo-head.astro: shared SeoHead helper used by every page
for canonical, OpenGraph, Twitter, and Article JSON-LD
- image-assets.ts: export ogDefaultImage for the SeoHead default card
- content.config.ts: tighten blog schema to enum category +
numeric readingTime, exclude underscore-prefixed files (_topics.md
is the blog-factory topic backlog, not a public post)
- content/blog/*.md: 5 launch posts (4 published + 1 internal _topics
backlog)
What's out:
- _components/blog-layout.astro: the placeholder layout with its own
mini-header was only used by the placeholder posts being removed;
drop it instead of leaving dead code
- 4 placeholder posts under content/blog/*.mdx that documented blog
scaffolding (test-post, atelier-zero-for-articles,
blog-routes-and-post-template, shipping-the-latest-note)
* feat(landing): refresh blog editorial layout
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>