mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
340 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dcfab797c2
|
[codex] Add stable nightly promotion gate (#962)
* Upload beta e2e spec reports to R2 * Expose beta report URLs in summary * Complete Indonesian deploy locale keys * chore: factor release workflow scripts * chore: bump packaged beta base version * test: wait for mac packaged runtime health * fix: capture mac packaged startup logs * chore: improve mac release build observability * fix: ad-hoc sign unsigned mac builds * chore: diagnose mac packaged startup * fix: relax unsigned mac launch signing * chore: improve mac launch diagnostics * chore: simplify beta mac release artifacts * fix: align packaged mac smoke launch config * fix: externalize mac daemon wasm dependency * chore: require signed stable mac releases * fix: use stable app version for nightly package builds * chore: clean release artifacts after publish * chore: publish beta reports as zip * ci: disable beta mac tools-pack cache * fix: skip mac framework binary symlinks when signing * fix: sign mac framework version bundles * ci: disable beta mac pnpm cache * chore: align stable release reports * ci: require matching nightly before stable release * ci: avoid mac pnpm cache for packaged smoke |
||
|
|
f951ccb612
|
fix: keep examples filter counts consistent (#949)
* fix: keep examples filter counts consistent * test: cover scoped examples scenario counts * test: satisfy examples fixture typing --------- Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com> |
||
|
|
ce5f20918c
|
test: cover model option rendering (#948)
* test: cover model option rendering * fix: strengthen model option regression coverage --------- Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com> |
||
|
|
2005a5b727
|
fix: add gpt-5.1 Codex picker options (#946)
* fix: add gpt-5.1 Codex picker options * test: clarify gpt-5.1 picker coverage --------- Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com> |
||
|
|
0f586c410d
|
Fix Cloudflare Pages custom domain lookup (#958)
* Support Cloudflare Pages custom domains without hiding pages.dev fallback Keep the default Pages preview as the first public link while optional owned-zone binding provisions DNS and Pages custom-domain state in parallel. Constraint: Cloudflare deploys must use the existing direct-upload API path with no Wrangler dependency. Constraint: pages.dev must stay visible even while custom-domain verification is pending. Rejected: Vercel custom-domain support | outside requested Cloudflare-only scope. Rejected: overwriting arbitrary CNAME records | risks taking over user-managed DNS. Confidence: high Scope-risk: moderate Directive: Do not expose providerMetadata through public deploy contracts; keep custom-domain DNS ownership checks conservative. Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts Tested: pnpm --filter @open-design/contracts build && pnpm --filter @open-design/contracts typecheck && pnpm --filter @open-design/contracts test Tested: pnpm --filter @open-design/web typecheck && pnpm --filter @open-design/web test -- providers/registry.test.ts components/FileViewer.test.tsx i18n/locales.test.ts Tested: pnpm i18n:check && pnpm guard && pnpm typecheck Tested: pnpm --filter @open-design/daemon build && pnpm --filter @open-design/web build && git diff --check Not-tested: real Cloudflare account/token/domain smoke test * Preserve Cloudflare fallback correctness under large accounts and races Constraint: Cloudflare Pages keeps pages.dev as the primary usable fallback while custom domains remain optional typed metadata. Rejected: Treating custom-domain DNS or binding failure as a top-level deployment failure | pages.dev can still be ready and usable. Confidence: high Scope-risk: moderate Directive: Keep custom-domain finality tied to Cloudflare Pages API active status plus URL reachability; do not expose providerMetadata. Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts; pnpm --filter @open-design/web test -- components/FileViewer.test.tsx i18n/locales.test.ts providers/registry.test.ts; pnpm --filter @open-design/daemon typecheck; pnpm --filter @open-design/web typecheck; pnpm i18n:check; git diff --check; pnpm guard; pnpm typecheck; pnpm --filter @open-design/daemon build; pnpm --filter @open-design/web build Not-tested: Real Cloudflare token/account/zone smoke test. * Keep impeccable design notes local Constraint: .impeccable.md is local assistant/design context and should not be part of the PR diff. Rejected: Keeping the file tracked while adding it to .gitignore | tracked files are not ignored by Git. Confidence: high Scope-risk: narrow Directive: Keep .impeccable.md untracked and ignored; do not rely on it for required project documentation. Tested: git check-ignore -v .impeccable.md; git diff --check Not-tested: Full workspace tests not rerun for ignore-only metadata change. * Use direct Pages domain lookup for custom bindings Cloudflare rejects list-style options on Pages custom-domain lookup in some accounts, so the deploy path now reads the selected hostname directly before creating a binding. This keeps pages.dev deployment success intact while avoiding a failed custom-domain branch caused by page/per_page query parameters. Constraint: Cloudflare Pages custom-domain lookup must not send unsupported page/per_page list options Rejected: Continue paginating /domains | Cloudflare returns invalid list options before the binding can be created Confidence: high Scope-risk: narrow Directive: Keep pages.dev as the primary deployment URL and treat custom-domain setup as a recoverable secondary branch Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts; pnpm --filter @open-design/daemon typecheck; pnpm guard; pnpm typecheck; git diff --check; pnpm --filter @open-design/daemon build Not-tested: Live Cloudflare deployment was not retriggered from the browser |
||
|
|
9f6a918796
|
Fix: windows inspect overlay (#944)
* fix(web): improve the hint overlay behaviour * feat(web): update inspect hint styling * fix(web): add title and aria-label for closing hint box * fix(web): remove the dead code from index.css * fix(web): make default state of openhintbox to true * fix(web): allow pointer events to pass through hint overlay |
||
|
|
c2facb0e02
|
fix: serve python files as text (#947)
Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com> |
||
|
|
208f09c60e
|
fix: settle completed runs and clean up shutdown children (#924)
* fix: clean up completed and shutting down runs * fix: bound daemon CLI shutdown Generated-By: looper 0.6.0 (runner=fixer, agent=codex) * fix: harden daemon shutdown cleanup Generated-By: looper 0.6.0 (runner=fixer, agent=codex) * fix: harden daemon shutdown cleanup Generated-By: looper 0.6.0 (runner=fixer, agent=codex) * test: align acp abort fake with typed child |
||
|
|
ef9ca7baff
|
fix(daemon): typecheck core server paths (#952) | ||
|
|
32d820e4ee
|
fix(daemon): typecheck leaf modules (#943)
* update drift * fix(daemon): typecheck leaf modules * fix(daemon): decode Qoder stdout buffers Generated-By: looper 0.5.6 (runner=fixer, agent=opencode) |
||
|
|
4f647f56ba
|
[codex] Optimize Composio connector previews (#907)
* Optimize Composio connector previews * Fix partial connector tool preview hydration * Cancel pending connector authorization on daemon * Preserve Composio cached tool counts * Avoid pending state after OAuth launch failure * Preserve static tool count fallback * Fix connector preview retry state * Remove Composio auth config metrics * Hydrate unknown connector tool previews * Fix remaining connector review threads * Stop failed connector preview spinner * Hydrate only targeted agent connectors |
||
|
|
e13adf2e63
|
feat(daemon): finalize design package endpoint (closes #450) (#832)
* feat(daemon): scaffold /api/projects/:id/finalize/anthropic (refs #450) Phase C of the PR 2 plan for issue #450: scaffold the route + module shape so subsequent phases (D-I) land function bodies and tests against a stable surface that already passes typecheck. What lands here: - apps/daemon/src/finalize-design.ts: module-level constants (DEFAULT_BASE_URL, DEFAULT_MAX_TOKENS=16000, INPUT_BODY_CAP_BYTES=384KiB, LOCK_FILENAME=.finalize.lock, OUTPUT_FILENAME=DESIGN.md, DEFAULT_TIMEOUT_MS=120s); inline interfaces for the request/response shape (kept out of packages/contracts per scope rules); two error classes - FinalizePackageLockedError (mirrors PR #493's TranscriptExportLockedError) and FinalizeUpstreamError (carries upstream HTTP status for the route's error mapping); function stub that throws "not yet implemented". - apps/daemon/tests/finalize-design.test.ts: vitest harness with describe.skip placeholder so the file imports cleanly. Real cases land in phases D-I. Default-import of node:fs (per memory: vi.spyOn cannot redefine on the frozen ESM Module Namespace; CJS exports object is mutable). - apps/daemon/src/server.ts: route handler at POST /api/projects/:id/finalize/anthropic, slotted next to the existing :id/deploy* family. Validates apiKey/model non-empty, optional baseUrl via the existing validateExternalApiBaseUrl closure (forbidden -> 403, invalid -> 400), optional maxTokens positive number; calls getProject (404 on miss); calls finalizeDesignPackage (which throws, caught and mapped to 500 for now); maps known error classes (FinalizePackageLockedError -> 409, FinalizeUpstreamError -> 502) pre-emptively. Path shape rationale (Bryan-confirmed): project-scoped path matches every sibling /api/projects/:id/* route in server.ts (deploy, deployments, deploy/preflight); provider-namespaced segment leaves a clean expansion line for /api/projects/:id/finalize/openai etc. as follow-ups. Field-name rationale: apiKey, baseUrl, model, maxTokens match ProxyStreamRequest verbatim (packages/contracts/src/api/proxy.ts:8-19) so a future caller can reuse the same body shape. baseUrl is optional here (intentional divergence from the proxy at server.ts which requires it) so standard Anthropic users do not need to set it; Bedrock / self-hosted-proxy users still can. Verification: pnpm --filter @open-design/daemon typecheck exits 0; finalize-design.test.ts loads cleanly with 1 skipped placeholder; no other tests touched. Refs nexu-io/open-design#450 (PR 2 scaffold; pipeline body in subsequent commits) * feat(daemon): transcript truncation helper for /finalize prompt Phase D of the PR 2 plan for issue #450: lands the helper that bounds the transcript section of the synthesis prompt. Why this exists: real-world signal at authoring time was a local project transcript already at 3.95 MB. Anthropic's claude-opus-4-7 context cap is roughly 200K tokens (~700 KB at typical density). Inserting an unbounded transcript would 4xx upstream on the first real call. This helper keeps the on-disk .transcript.jsonl lossless (PR #493's contract) while making the prompt-inclusion bounded. Strategy: - Cap output at INPUT_BODY_CAP_BYTES (384 KiB) so the prompt has room for the system prompt + design system body + current artifact + room for the synthesis output. - Always preserve the header line - it carries projectId, schemaVersion, conversation/message counts, attachment counts; synthesis quality depends on knowing the original sizes. - Split equal byte budgets between head and tail so both project genesis and most-recent intent survive. Two thinking segments separated only by mid-session truncation lose the same kind of boundary that PR #493 preserves between thinking blocks - that's accepted; smarter semantic chunking is a follow-up. - Insert a single `{"kind":"truncated","reason":"size","omittedBytes":N}` sentinel JSON line between the head and tail so a synthesis consumer can detect the gap. omittedBytes is the difference between the original UTF-8 byte length and the output's UTF-8 byte length. - If the head + tail budgets together cover the whole body (e.g. all message lines are tiny), no marker is emitted - the output is the input verbatim. Tests: - "returns the input verbatim when the JSONL fits under the 384 KiB cap" pins that small transcripts pass through unchanged with no marker. - "head+tail truncates with a single marker line when the JSONL exceeds the 384 KiB cap" pins that output is bounded, header survives, exactly one marker emitted with non-zero omittedBytes, both ends of the body preserved, and at least one middle message omitted. Suite delta: +2 tests in finalize-design.test.ts. Refs nexu-io/open-design#450 * fix(daemon): resolve noUncheckedIndexedAccess in truncateTranscriptForPrompt D1 (0eaa123) shipped with `body[headIndex]` and `body[i]` typed as `string | undefined` under TypeScript's `noUncheckedIndexedAccess` strict mode. Local typecheck would have caught it but the prior verification piped through `tail` which masked the non-zero exit code of `tsc`. Coalesce each access via `?? ''` (the array is from `String.split('\n')` so undefined elements are not actually reachable; the coalesce is a type-narrowing convenience, not a behavior change). Verification: `pnpm --filter @open-design/daemon typecheck` exits 0; `pnpm --filter @open-design/daemon test finalize-design` shows 2/2 + 1 skipped, identical to the pre-fix run. Refs nexu-io/open-design#450 * feat(daemon): current-artifact resolver for /finalize Phase E of the PR 2 plan for issue #450: resolves which artifact (if any) accompanies the transcript + design system in the synthesis prompt. Priority order (Bryan-locked in plan §6): 1. The file referenced by tabs.is_active = 1 IF an <name>.artifact.json sidecar exists on disk. Sidecar presence is the discriminator: an inferred manifest from `inferLegacyManifest` (e.g. for a bare .html with no sidecar) does NOT count, and an active tab pointing at a non-artifact file (.md, .txt) falls through. 2. Newest project file with a real .artifact.json sidecar, sorted by manifest.updatedAt descending. Files without an updatedAt sort last so legacy pre-streaming manifests do not get accidentally promoted. 3. Returns null - "no artifact in scope". The Phase H caller will emit `artifact: null` in the response and the prompt's "Current artifact" section will read "none". Sidecar presence is checked via `existsSync` on the on-disk path, NOT via the `artifactManifest` field returned by readProjectFile/listFiles (those run inferLegacyManifest as a fallback for known kinds, which would otherwise cause a bare .html with no sidecar to look like an artifact). Tests: - "returns the active-tab artifact when its sidecar is present, even if a newer artifact exists elsewhere": pinned.html (older updatedAt) is in the active tab; newer.html (newer updatedAt) is not. Resolver returns pinned.html - intent (active tab) beats recency. - "falls through to newest .artifact.json when active tab points at a non-artifact file": README.md is the active tab (no sidecar); design.html has a real sidecar. Resolver falls through and returns design.html. - "returns null when no active tab and no .artifact.json sidecars exist": only a README.md is in the project; no tabs row. Resolver returns null. Suite delta: +3 tests in finalize-design.test.ts (5 active total). Refs nexu-io/open-design#450 * feat(daemon): synthesis prompt construction for /finalize Phase F of the PR 2 plan for issue #450: builds the system + user prompts that get sent to Anthropic's Messages API in the synthesis call. Pure function; no IO, no side effects. System prompt (literal, stored as a module-level constant): instructs Claude to emit a DESIGN.md document with a fixed 7-heading structure (# DESIGN.md / ## Summary / ## Brand & Voice / ## Information Architecture / ## Components & Patterns / ## Visual System / ## Open Questions / ## Provenance). The Provenance section is required to list project ID, design system, current artifact, transcript message count, and the UTC generation timestamp. User prompt (built at runtime): structured payload with the truncated transcript JSONL, the design system body, and the current artifact body, each under a ## heading. Missing inputs (no design system selected, no artifact in scope) produce explicit "none" headings + parenthetical placeholder body so Claude does not hallucinate content for absent sections. Truncation is the caller's concern - this function does not re-truncate. The caller (Phase H pipeline) feeds in a JSONL that has already been bounded by truncateTranscriptForPrompt. Tests: - "includes the transcript JSONL verbatim and the generation context": pins all section headings, the transcript body verbatim, the design system body verbatim, the artifact body verbatim, and every generation-context line. - "falls back to \"none\" + parenthetical when no design system is selected": designSystemId=null and designSystemBody=null -> heading reads "## Active design system: none" with the parenthetical body. - "falls back to \"none\" + parenthetical when no artifact is in scope": artifact=null -> heading reads "## Current artifact: none" with the parenthetical body. Suite delta: +3 tests in finalize-design.test.ts (8 active total). Refs nexu-io/open-design#450 * feat(daemon): Anthropic call + retry strategy for /finalize Phase G of the PR 2 plan for issue #450: lands the upstream Claude Messages API call with a single transient-error retry, plus the response extractor that turns Anthropic's content array into the DESIGN.md body. What lands here: - appendVersionedApiPath: inlined from the connectionTest helper at apps/daemon/src/connectionTest.ts:188-195 (it is not exported there). Appends /v1/messages when the base URL has no /vN segment, otherwise appends /messages directly. Same semantics; ~5 lines. - callAnthropicWithRetry: POSTs to <base>/v1/messages with the canonical Anthropic headers (content-type, x-api-key, anthropic-version: 2023-06-01) and body shape ({ model, max_tokens, system, messages, stream:false }). One retry on transient (HTTP 429 or 5xx); on terminal failure throws FinalizeUpstreamError carrying the upstream HTTP status and raw body text. The route handler in Phase I maps status to AUTH_FAILED / RATE_LIMITED / UPSTREAM_FAILED and runs the body through redactSecrets before exposing it as `details`. - extractDesignMd: concatenates content[].text for every block where type === 'text', preserving order. Throws FinalizeUpstreamError(502) on three malformed-response shapes: non-object payload, missing content array, zero text blocks. The route handler maps the throw to 502 UPSTREAM_FAILED so synthesis cannot land a half-empty DESIGN.md on disk. - Test-only `_sleepMs` injection on the call params so the retry-delay sleep is instant under vitest. Default sleep uses setTimeout. Retry posture (1 retry on transient) is opinionated; the maintainer's "standard exponential backoff" answer was directional and a single retry matches the existing daemon's posture (transcript export and connectionTest do zero retries) while staying inside the daemon's blocking-fast posture for /finalize. Tests: - callAnthropicWithRetry: throws on 401 with no retry; retries once on 429 and resolves on second 200; throws after both 5xx attempts; propagates AbortError when signal is pre-aborted. - extractDesignMd: concatenates ordered text blocks; throws on missing content array; throws on content with zero text blocks. A spurious typecheck error from `exactOptionalPropertyTypes` (signal typed as AbortSignal | undefined where RequestInit expects AbortSignal | null) was resolved by conditionally spreading signal into the RequestInit literal. Suite delta: +7 tests in finalize-design.test.ts (15 active total). Refs nexu-io/open-design#450 * feat(daemon): wire /finalize pipeline end-to-end Phase H of the PR 2 plan for issue #450: stitches together every phase D-G primitive into the full finalizeDesignPackage pipeline that the route handler in Phase I will expose over HTTP. Pipeline (in execution order, all inside a try/finally that always releases the lockfile): 1. getProject(db, projectId): defensive 404 (the route validates first; this throw catches direct CLI/script callers). 2. mkdirSync(<projectDir>, { recursive: true }): some projects have DB rows but no on-disk dir yet (PR #493's same fix). 3. fs.openSync(.finalize.lock, 'wx'): EEXIST -> FinalizePackageLockedError (mirror PR #493's TranscriptExportLockedError). 4. exportProjectTranscript(db, projectsRoot, projectId, { now }): produces .transcript.jsonl on disk; we read the body and run it through truncateTranscriptForPrompt to bound the prompt-inclusion size. 5. readDesignSystem(designSystemsRoot, designSystemId): returns null when the project has no design_system_id selected, when the design system directory does not exist, or when the DESIGN.md file is missing. 6. resolveCurrentArtifact(db, projectsRoot, projectId): active tab -> newest .artifact.json by manifest.updatedAt -> null. 7. buildSynthesisPrompt({...}): system + user prompt (per Phase F). 8. callAnthropicWithRetry({...}): one retry on 429/5xx; throws FinalizeUpstreamError on terminal failure. 9. extractDesignMd(payload): concatenates content[].text blocks; throws FinalizeUpstreamError(502) on malformed shape. 10. Atomic write: writeFileSync({flag:'wx'}) -> reopen for fsync -> rename. Errors unlink tmp before rethrowing. 11. Lock release in finally (always closeSync + unlinkSync). Bounded blocking: the function uses its own AbortController + 120s timeout when the caller does not supply a signal. Caller-supplied signal takes precedence. Type tightening: switched the local Db interface to `type Db = Database.Database` (better-sqlite3) so the function signature is compatible with `exportProjectTranscript`'s typed parameter. Source file already had a `better-sqlite3` import in claude-design-import area of the daemon, so no new dependency. Tests: - "writes DESIGN.md atomically on the happy path": end-to-end with seeded project + conversation + 2 messages + design system on disk; asserts file at exact path + body bytes match the fetch mock. - "response carries every documented field with correct types": designMdPath/bytesWritten/model/inputTokens/outputTokens/artifact/ transcriptMessageCount/designSystemId all present and typed. - "emits design system 'none' in the prompt when no design_system_id is set": fetch mock asserts on the body it receives. - "throws FinalizePackageLockedError when .finalize.lock is already held": pre-create lockfile; assert throw + DESIGN.md not written + pre-existing lock NOT unlinked (we did not own it). - "replaces an existing DESIGN.md atomically on a second finalize": inject a sentinel between two finalize calls; assert sentinel is gone after second run. - "cleans up tmp file AND lock file on every error path": mock fs.writeFileSync to throw on the tmp path; assert no DESIGN.md.tmp.* remain, no DESIGN.md, no .finalize.lock. - "uses the default https://api.anthropic.com baseUrl when baseUrl is omitted": fetch URL begins with the default; baseUrl=undefined path. vi.restoreAllMocks() now runs in afterEach so the writeFileSync spy from the cleanup test does not leak into subsequent tests. Suite delta: +7 tests in finalize-design.test.ts (22 active total). Refs nexu-io/open-design#450 * feat(daemon): /finalize HTTP route handler + error mapping Phase I of the PR 2 plan for issue #450: replaces the Phase C stub's catch-all 500 with status-aware error mapping that surfaces the right HTTP status + error code for each documented failure mode, and adds HTTP-layer tests that boot startServer to exercise the route's validation branches. Route handler changes: - :id format guard: an inline regex matching isSafeId at apps/daemon/src/projects.ts:556-558 rejects unsafe ids with 400 BAD_REQUEST before any DB or filesystem work. Without this, an id like 'bad!id' would either fail getProject as 404 (wrong code) or reach the function and throw 'invalid project id' (mapped to 500). - FinalizeUpstreamError mapping is now status-aware: - upstream 401 -> 401 AUTH_FAILED - upstream 429 -> 429 RATE_LIMITED - upstream 5xx (or our own 502 sentinel for malformed responses) -> 502 UPSTREAM_FAILED In all cases the upstream raw text is run through redactSecrets so the apiKey cannot leak through `details` even if the upstream echoes the inbound headers. - AbortError mapping: when the 120s AbortController fires (or the caller pre-aborted the signal), surface as 503 TIMEOUT. - Default case: console.error the error per daemon convention; client sees 500 INTERNAL with the message routed through redactSecrets. - Imported redactSecrets alongside the existing connectionTest imports (apps/daemon/src/server.ts:51). HTTP-layer tests (boot startServer({port:0,returnServer:true}) once in beforeAll, mirror the proxy-routes.test.ts pattern): - "400 BAD_REQUEST when baseUrl is not a valid URL (test #13)": baseUrl='not-a-url'. - "403 FORBIDDEN when baseUrl points at a private internal IP (test #14)": baseUrl='http://10.0.0.1'. Note: validateBaseUrl explicitly allows loopback (for local OpenAI-compatible servers) and only blocks non-loopback private IPs (10/8, 172.16/12, 192.168/16, fc00::/7, fe80::/10). - "400 BAD_REQUEST when apiKey is missing (test #15)": apiKey omitted. - "400 BAD_REQUEST when :id contains characters outside the safe-id regex (test #16)": id='bad!id' contains '!' which is not in [A-Za-z0-9._-]. Suite delta: +4 tests (26 active in finalize-design.test.ts). Full daemon suite: 1078/1078 pass; baseline+26 (the +5 above plan target reflects retry+extract split into more granular unit tests than originally enumerated; all real, none skipped). Refs nexu-io/open-design#450 * fix(daemon): tighten isSafeId to reject pure-dot project ids Addresses the P1 path-traversal finding from @lefarcen on PR #832 (https://github.com/nexu-io/open-design/pull/832#discussion_r3202512644). The pre-fix `isSafeId` at apps/daemon/src/projects.ts:556-558 used regex `/^[A-Za-z0-9._-]{1,128}$/` which permitted pure-dot ids (`.`, `..`, `...`) because `.` is in the character class. `projectDir` and `resolveProjectDir` both delegated to `isSafeId`, so an id of `..` would resolve to the PARENT of `.od/projects/` via `path.join`. Threat model (per @lefarcen): - An attacker creates a project row whose stored id is `..` (or another pure-dot variant) — for instance via a workflow that writes the row directly without going through the API. Subsequent finalize/write ops keyed by that id then escape the project tree. - A direct CLI / scripted caller passing `..` as the project id reaches the function without HTTP normalization saving us. (Express normalizes %2e%2e to .. and collapses path segments, which yields 404 for the URL `/api/projects/%2e%2e/...` in practice — but that's Express's protection, not ours.) Fix: - isSafeId now explicitly rejects pure-dot ids (`/^\.+$/.test(id)`) before the char-class regex check. Empty string and inputs longer than 128 chars are also rejected explicitly so the function fails closed on edge cases. - isSafeId is now exported from apps/daemon/src/projects.ts so the /finalize route handler in apps/daemon/src/server.ts can use the same validator instead of re-implementing the regex inline. This prevents drift between the route guard and the projectDir guard, which was how this hole originally appeared. Tests (in finalize-design.test.ts because that's where the threat was flagged; isSafeId is daemon-wide so a dedicated test file would also work): - isSafeId rejects `.`, `..`, `...`, `....` - isSafeId rejects ids with `/`, `\`, `!`, leading whitespace - isSafeId rejects empty string and >128 chars - isSafeId rejects non-string inputs (null/undefined/number) - isSafeId accepts plain ids, ids with mid-string dots, UUIDs, single chars Suite delta: +7 tests (33 active in finalize-design.test.ts). Full daemon suite: 1085/1085. Refs nexu-io/open-design#832 * fix(daemon): address PR #832 P1 findings — imported folders + network 502 Addresses two of the three P1 findings from @lefarcen on PR #832: 1. Imported-folder projects route DESIGN.md to metadata.baseDir (https://github.com/nexu-io/open-design/pull/832#discussion_r3202512656, also flagged independently by @chatgpt-codex-connector at #discussion_r3202430470) The pipeline previously called `projectDir(projectsRoot, projectId)` unconditionally, which resolves to `.od/projects/<id>`. For projects created via /api/import/folder the project row's `metadata.baseDir` carries the user's actual folder; without threading metadata through, finalize would silently land DESIGN.md in the hidden daemon data dir and the current-artifact resolver would miss the user's real files. Fix: switch from `projectDir` to `resolveProjectDir(projectsRoot, projectId, metadata)` in both `finalizeDesignPackage` and `resolveCurrentArtifact`. Thread `project.metadata` (from `getProject`'s normalized row) through both call paths. The resolver gets a new optional `metadata` parameter; native projects pass null and get identical behavior. 2. Network failures and JSON parse errors now map to 502 UPSTREAM_FAILED (https://github.com/nexu-io/open-design/pull/832#discussion_r3202512661) Pre-fix, only HTTP-non-OK responses were wrapped as FinalizeUpstreamError. DNS failures (ECONNREFUSED, ENOTFOUND), fetch TypeErrors, and `response.json()` SyntaxErrors fell through to the route's catch-all and surfaced as 500 INTERNAL — incorrect: those are upstream-level failures, not daemon bugs. Fix: - Wrap callAnthropicWithRetry in a try/catch that passes FinalizeUpstreamError and AbortError through verbatim, but rewraps any other thrown error as FinalizeUpstreamError(502, '', message). - Wrap response.json() in a try/catch that rewraps SyntaxError as FinalizeUpstreamError(502, '', "upstream Anthropic returned non-JSON body: ..."). - The route handler's existing FinalizeUpstreamError mapping then correctly maps these to 502 with the message in `details` (run through redactSecrets first). Tests: - "writes DESIGN.md under metadata.baseDir for imported-folder projects": inserts a project row with metadata.baseDir pointing at a user-folder temp dir; asserts result.designMdPath lands there AND the hidden .od/projects/<id> dir does NOT contain a DESIGN.md. - "rewraps fetch network rejection as FinalizeUpstreamError(502)": fetchImpl throws TypeError with cause.code='ENOTFOUND'; assert thrown error has name=FinalizeUpstreamError and status=502. - "rewraps 200 with non-JSON body as FinalizeUpstreamError(502)": fetchImpl returns 200 with text/html body; response.json() throws SyntaxError internally; assert FinalizeUpstreamError(502). Suite delta: +3 tests (36 active in finalize-design.test.ts). Full daemon suite: green at last check; will re-verify before push. Refs nexu-io/open-design#832 * refactor(daemon): move /finalize DTOs to contracts + map error codes + validate active-tab Addresses the P2 and P3 findings from @lefarcen on PR #832: P2 — Error codes + DTOs not in packages/contracts https://github.com/nexu-io/open-design/pull/832#discussion_r3202512673 Reverses my plan's locked decision #10 ("no contracts changes in this PR; inline the request/response types"). That rule came from the predecessor PROMPT brief's anti-pattern table; @lefarcen's review is fresher signal and supersedes it. Drift risk between the daemon's inline types and any future PR 3 web client is real. - New contracts module: packages/contracts/src/api/finalize.ts with FinalizeAnthropicRequest / FinalizeArtifactRef / FinalizeAnthropicResponse. Re-exported from the package root and made addressable via `@open-design/contracts/api/finalize` subpath. - Daemon source imports the canonical types from contracts and re-exports the public type names so internal references keep working without touching every call site. - Daemon-local error codes remapped to existing ApiErrorCode union members (apps/daemon/src/server.ts), per @lefarcen's suggested mapping: FINALIZE_IN_PROGRESS -> CONFLICT AUTH_FAILED -> UNAUTHORIZED UPSTREAM_FAILED -> UPSTREAM_UNAVAILABLE TIMEOUT -> UPSTREAM_UNAVAILABLE (status 503) INTERNAL -> INTERNAL_ERROR HTTP status codes are unchanged; only the `code` field in the error JSON body changed. P3 — Active-tab name not validated before sidecar probe https://github.com/nexu-io/open-design/pull/832#discussion_r3202512684 resolveCurrentArtifact now runs the active tab's name through validateProjectPath BEFORE composing it into a path.join expression. An invalid tab (traversal segments, absolute path, null byte, reserved segment) causes resolveCurrentArtifact to fall through to the newest-artifact branch rather than abort or probe outside the project directory. Tests: - "falls through (does not throw) when active tab name contains traversal segments": injects a malformed `tabs.name = '../../../etc/passwd'` row directly via SQL (bypassing production tab-creation validation), seeds a real artifact, asserts the resolver returns the real artifact rather than the malformed name. Suite delta: +1 test (37 active in finalize-design.test.ts). Full daemon suite: 1089/1089 green. Refs nexu-io/open-design#832 * fix(contracts): publish /api/finalize as standalone runtime entrypoint Addresses @mrcfps's CI-red review on PR #832 (https://github.com/nexu-io/open-design/pull/832, inline comment on packages/contracts/package.json). The previous J3 commit added `./api/finalize` as a type-only subpath: the entry had only a `types` field, no `default`. That broke the contracts package-runtime gate (packages/contracts/tests/package- runtime.test.ts:38-47) which asserts every exports entry exposes both a `.mjs` runtime and a `.d.ts` types target. mrcfps proposed two fixes; this commit takes path B — make finalize a first-class published module rather than a type-only re-export from the package root. Path B vs path A (a peer-AI second opinion via /collaborate confirmed): under NodeNext + ESM with exports-map semantics, TypeScript validates re-exported symbols against the published module-identity surface. Because the previous J3 had `./api/finalize` neither declared as an exports-map entry nor materialized as a standalone .mjs, TS omitted the re-exported names during package boundary analysis. Even at runtime `import('@open-design/contracts').FINALIZE_SCHEMA_VERSION` worked from the bundled index.mjs but the type-checker rejected it. Path B aligns the runtime and declaration surfaces. Changes: - packages/contracts/esbuild.config.mjs: add `./src/api/finalize.ts` to entryPoints so dist/api/finalize.mjs is generated as a standalone module rather than only inlined into the bundled root. - packages/contracts/package.json: re-add `./api/finalize` to the exports map with both `default: ./dist/api/finalize.mjs` AND `types: ./dist/api/finalize.d.ts`. Mirrors `./api/connectionTest`'s shape (the canonical pattern for first-class submodule entries). - packages/contracts/src/api/finalize.ts: keep the runtime export `FINALIZE_SCHEMA_VERSION = 1` (giving the standalone module a real value to emit beyond the type-only interfaces) and update the doc-comment now that the standalone .mjs is wired. - apps/daemon/src/finalize-design.ts: switch the type import from the inline declarations introduced in the prior J3 fallback to `import type { ... } from '@open-design/contracts/api/finalize'`. Re-export the names so internal references inside finalize-design.ts keep working without touching every call site. Verified: - node --input-type=module -e "import('@open-design/contracts/api/finalize').then(m=>console.log(JSON.stringify(Object.keys(m))))" prints ["FINALIZE_SCHEMA_VERSION"] — runtime resolution clean. - pnpm --filter @open-design/contracts test: 6/6 (including both package-runtime.test.ts cases on the rebuilt exports map). - pnpm --filter @open-design/daemon typecheck: exits 0. - pnpm --filter @open-design/daemon test: 1089/1089 (no regression vs the prior J3 number). Refs nexu-io/open-design#832 --------- Co-authored-by: DevForgeAI CI/CD Engineer <devforge-ai@development.ai> |
||
|
|
1e8926271b
|
Harden security scan findings and upgrade dependencies (#806)
* feat: add accent color control and launcher for Open Design * fix: remove launcher binary from PR * test: cover accent appearance edge cases * Harden security scan findings and upgrade deps * Address proxy security review * Pin jsdom for web test stability --------- Co-authored-by: ferasbusiness666 <ferasbusiness666@users.noreply.github.com> Co-authored-by: lefarcen <935902669@qq.com> |
||
|
|
d592f6087f
|
feat(mcp): external MCP client with daemon-managed OAuth and 39 design-focused templates (#898)
* feat(mcp): add external MCP client with daemon-managed OAuth and 17 design-focused templates
Open Design now acts as an MCP CLIENT and surfaces tools from third-party
MCP servers to the underlying agent (Claude Code, Hermes, Kimi).
Daemon
- New mcp-config / mcp-oauth / mcp-tokens modules: persist server entries
to .od/mcp-config.json, run the OAuth dance for HTTP/SSE servers
end-to-end on the daemon (so cloud deployments work and tokens
survive across turns), and inject Authorization: Bearer headers into
the per-spawn .mcp.json the daemon writes for Claude Code (or the
ACP mcpServers map for Hermes/Kimi).
- /api/mcp/servers and /api/mcp/oauth/{start,status,disconnect}
endpoints, plus spawn-time wiring in agents that hands the configured
servers to the active agent CLI.
- System-prompt directive for connected external MCPs so the model
does not chase Claude Code's synthetic *_authenticate /
*_complete_authentication tools when the Bearer is already pinned.
Web
- Settings -> External MCP servers panel with per-row OAuth Connect /
Disconnect / Refresh affordances and per-row template hints.
- New "Add server" picker categorized into 7 groups
(image-generation, image-editing, web-capture, ui-components,
data-viz, publishing, utilities) with a search box, sticky close
button, collapsible <details> sections (auto-expand on search),
60vh capped scroll region, and a pinned Custom-server footer.
- ChatComposer /mcp slash and MCP picker button forward to the new
Settings tab; AssistantMessage renders MCP tool calls inline;
markdown autolinker handles bare http(s) URLs (incl. OAuth links)
before italic markers so OAuth callback URLs do not get
italic-fragmented mid-token.
Contracts
- packages/contracts/src/api/mcp.ts owns the wire shapes
(McpServerConfig, McpTemplate with stable McpTemplateCategory
enum, McpServersResponse, OAuth start/status/disconnect bodies, the
postMessage payload from the OAuth callback).
Templates (17 built-in)
- image-generation: Higgsfield (OpenClaw, OAuth HTTP), Pollinations,
Allyson (animated SVG), AWS Bedrock Image (uvx).
- image-editing: Imagician, ImageSorcery.
- web-capture: just-every screenshot-website-fast, ScreenshotOne.
- ui-components: 21st.dev Magic, shadcn/ui, FlyonUI.
- data-viz: AntV Chart, Mermaid.
- publishing: EdgeOne Pages.
- utilities: Filesystem, GitHub, Fetch.
Tests
- apps/daemon/tests/mcp-{config,oauth,tokens,spawn}.test.ts cover
storage round-trip, OAuth helpers, token persistence, spawn-time
wiring, every template's transport / command / args / env-field
invariants, and the canonical category enum.
- apps/web/tests/runtime/markdown.test.tsx covers the new autolinker
ordering rules.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(mcp): add 21 more design-focused templates and a `design-systems` category
Expands the built-in MCP picker from 17 to 38 templates so users can compose
the full Open Design craft loop (design-system intake → generate → edit →
audit → publish) without leaving the Settings dialog. Every install spec is
verified live against the upstream README; templates that needed Go binaries,
multi-step `init` ceremonies, or massive runtime stacks (PostgreSQL + Redis
+ Ollama) are intentionally deferred so picking a template still resolves to
a working server in one click.
New `design-systems` category between `web-capture` and `ui-components`
(reflects the upstream-of-components position in the workflow). Mirrored in
`McpTemplateCategory` on both contracts and daemon, and `CATEGORY_ORDER` on
the web side.
New templates by category:
- image-generation (+4): prompt-to-asset (icons / favicons / OG / logos with
free-tier routing across Cloudflare AI / NVIDIA NIM / HF / Stable Horde),
Nano Banana (hosted streamable HTTP, virtual try-on + product placement),
Seedream (hosted streamable HTTP, ByteDance Seedream v3-v5 + SeedEdit),
fal.ai (uvx, 600+ models incl. FLUX / Kling / Hunyuan / MusicGen).
- image-editing (+3): Photopea (34 layered-editor tools — closes the PSD
gap), Topaz Labs (AI upscale / denoise / sharpen), Transloadit (86+ media
pipeline robots).
- web-capture (+1): Pagecast (browser → demo GIF / MP4 with auto-zoom).
- design-systems (+4, NEW category): Figma-Context (Framelink, designs →
code), Design Token Bridge (Tailwind ⇄ CSS ⇄ Figma ⇄ M3 / SwiftUI / W3C
DTCG + WCAG contrast), Design System Extractor (Storybook scrape),
Aesthetics Wiki (cottagecore / dark-academia / y2k / … moodboards).
- data-viz (+2): MCP Dashboards (45+ chart types + KPI dashboards),
Excalidraw Architect (hand-drawn architecture diagrams).
- publishing (+6): PageDrop, PDFSpark, OGForge, QRMint, Slideshot
(HTML → PDF / PPTX / PNG with 7 themes), Deckrun (Markdown → PDF / video,
hosted free tier with no key required).
- utilities (+1): A11y axe-core (WCAG 2.0/2.1/2.2 + color-contrast + ARIA).
Tests cover every new template's wiring (command, args, env / header
required-vs-optional, secret flag), the category enum invariant, and
in-category declaration order for image-generation, design-systems and
publishing buckets where the order is what users see in the picker. 21 new
test cases pass; full mcp-config suite is green.
Templates intentionally deferred (documented in PR body): figma-use
(needs Figma desktop with --remote-debugging-port=9222), m-moire (multi-step
`memi suite init` + daemon ceremony), gemini-media-mcp + trident-mcp (Go
binaries — no npx / uvx path), Pixelle-MCP (full app with web UI + ComfyUI
backend), storybook-addon-mcp (lives inside user's Storybook, not standalone),
primitiv (multi-step init / build / serve), ReftrixMCP (PostgreSQL + Redis +
Ollama + DINOv2), narasimhaponnada/mermaid (overlap with peng-shawn).
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(mcp): add figma-use template (write designs from chat) under design-systems
figma-use is the natural counterpart to Figma-Context already in this PR:
where Framelink reads Figma designs into the model, figma-use writes back
into the canvas (90+ tools — create frames / text / components / variants,
render JSX into Figma, export PNG/SVG, query nodes via XPath, lint for
WCAG / auto-layout / hardcoded colors, analyze design systems).
Wired as an HTTP MCP template (`http://localhost:38451/mcp`) because
`figma-use mcp serve` only exposes HTTP — there's no stdio mode in the
upstream `serve.ts`. No API key. Two prerequisites the user owns are
spelled out in the description so picking the template still resolves to
a working server: (1) start Figma with `--remote-debugging-port=9222`
(or `figma-use daemon start --pipe` on Figma 126+), and (2) leave
`npx figma-use mcp serve` running in a terminal.
Inserted between `design-system-extractor` and `aesthetics-wiki` so the
design-systems category reads as a workflow: read existing design (Figma
Context) → translate tokens (Token Bridge) → extract from Storybook
(Extractor) → write back to Figma (figma-use) → break creative block
(Aesthetics Wiki).
Tests cover the new template's transport (`http`), endpoint URL, the
empty header-fields invariant (no auth required), and bump the
design-systems group order to include it.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(settings): i18n the External MCP / MCP server / Connectors sidebar entries and make the dialog header track the active section
The External MCP sidebar entry this PR introduces was hardcoded English
("External MCP / Add MCP tools (Higgsfield, GitHub…)"). Same for the
adjacent Connectors and MCP server entries. The dialog header was also
pinned to "Execution & model" copy, so opening Settings → External MCP
showed a header that lied about which section the user was on.
Adds six translation keys — `settings.connectorsTitle/Hint`,
`settings.mcpServerTitle/Hint`, `settings.externalMcpTitle/Hint` — and
translates them across all 17 locales (ar, de, en, es-ES, fa, fr, hu, id,
ja, ko, pl, pt-BR, ru, tr, uk, zh-CN, zh-TW).
`SettingsDialog` now derives the header title/subtitle from the active
section (11 sections total) instead of a single hardcoded pair, so each
section renders an honest header.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(e2e): pin level: 3 on dialog heading lookups for Pets and Connectors
CI's Validate workspace job (#1479) failed two Playwright cases with the
strict-mode violation:
getByRole('dialog').getByRole('heading', { name: 'Pets' })
resolved to 2 elements:
1) <h2>Pets</h2>
2) <h3>Pets</h3>
Same root cause as the unit-test fix already in this PR: the dynamic
dialog `<h2>` now echoes the section's own `<h3>` because the dialog
header tracks the active section. Disambiguate to `level: 3` so each
assertion still pins the section heading specifically (which is what
the test intends to verify).
Audit of the rest of e2e/ for `dialog.getByRole('heading', ...)` —
settings-api-protocol.test.ts looks for "OpenAI API" / "Anthropic API"
section h3s which never appear in the dialog `<h2>` (always
"Execution & model"), so those stay safe.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(mcp): bind OAuth refresh to the issuing client and skip stale tokens
Persist the OAuth client context (token endpoint, client_id, client_secret,
issuer, redirect_uri, resource) alongside the bearer token so refresh hits
the same client the refresh_token was bound to (RFC 6749 §6). The previous
refresh path re-ran beginAuth with a dummy OOB redirect URI, which kept
getOrRegisterClient from finding the original DCR client and made
providers reject the refresh on the next chat turn. Refreshes now reuse
the persisted endpoint/client pair directly.
Also stop injecting expired access tokens at spawn time when refresh is
unavailable or fails. Pinning a stale Bearer made every Claude MCP call
401 while the prompt still treated the server as connected; on that path
we now skip the entry and let the UI surface a reconnect.
Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
2b5ea36f21
|
feat: add ib-pitch-book deck skill (Pitch Agent port) (#888)
* feat(skills): add ib-pitch-book deck skill (Pitch Agent port) - Add investment-banking strategic-alternatives pitch book skill adapted from anthropics/financial-services Pitch Agent (Apache-2.0) - Ship self-contained example.html (fictional NorthPeak / Hartfield case) - Add references: compliance, attribution, conventions, P0/P1/P2 checklist - Document in CHANGELOG [Unreleased] Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ib-pitch-book): align comps copy and DCF sensitivity base cell - Trading comps: narrative now matches table (12.5× vs 12.4× median; explain via growth/mix) - DCF: base-case sensitivity cell at 9.0% WACC × 2.5% g = $56.40, matching implied equity / share Co-authored-by: Cursor <cursoragent@cursor.com> * fix: register ib-pitch-book in i18n fallbacks; add demo disclaimer banner Co-authored-by: Cursor <cursoragent@cursor.com> * fix: address ib-pitch-book review feedback Co-authored-by: Cursor <cursoragent@cursor.com> * fix: prevent ib-pitch-book dense slide overflow Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
1d1df52f3b
|
feat(skills/live-artifact): add 7 example dashboards + contract demo (#716)
* feat(skills/live-artifact): add 7 example dashboards + contract demo
Seven self-contained HTML prototypes under skills/live-artifact/examples/,
each with a distinct visual identity and built-in interactivity for video
demos:
stock-dashboard.html - Bloomberg-style trading floor (dark)
crypto-dashboard.html - DeFi/web3 cyber terminal with on-chain ribbon
crm-table-live.html - multi-dim CRM with Grid/Kanban/Gallery/Calendar
view switcher (light productivity)
monday-operator-live.html - editorial Monday-morning briefing (paper)
competitor-radar-live.html - mission-control radar with rotating sweep
and RGB threat tiers
baby-health-live.html - soft pastel parental panel
stock-portfolio-live/ - full live-artifact contract example: 102
escaped html_template_v1 bindings + 7
data-od-repeat blocks, ready to register
via 'tools live-artifacts create'
Each interactive HTML carries refresh-with-flash, view switching, AI
panel regeneration, clickable rows/cards that mutate state, and toast
notifications. Self-contained - only Google Fonts as external dep.
stock-portfolio-live/ demonstrates the daemon contract: template.html +
data.json + artifact.json + provenance.json. Refresh runners can rewrite
data.json without re-authoring the template.
* fix(skills/live-artifact): address PR #716 review feedback
- Unroll data-od-repeat blocks into indexed data.* bindings so renderHtmlTemplateV1 can interpolate them (it does not expand data-od-repeat or repeat-local aliases like {{t.label}}).
- Rename catalysts[].body to catalysts[].text to satisfy the bounded JSON validator's forbidden-key list (body is rejected case-insensitively); update template binding accordingly.
Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)
* fix(skills/live-artifact): make stock-portfolio provenance.json contract-compliant
- generatedBy: free-form string -> "agent" (LiveArtifactProvenanceGenerator enum)
- sources[].kind -> sources[].type with LiveArtifactProvenanceSourceType enum values
(connector for brokerage/quotes connectors, derived for AI recommendation)
- Drop non-contract per-source `note` and top-level `summary`/`transformations`/
`refreshContract`/`safetyNotes` fields; preserve their content under the
contract-allowed `notes` field so the example survives schema validation.
Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)
* fix(skills/live-artifact): use strict ISO-8601 generatedAt in provenance
The daemon's `validateIsoDate` requires `Date.toISOString()` round-trip
equality, so timezone-offset notation like `2026-05-06T14:32:18-05:00`
fails validation even though it parses. Switch to the canonical UTC form
`2026-05-06T19:32:18.000Z` (same instant), which the validator accepts.
* feat(skills): surface examples/*.html as derived skill cards + Live filter
A skill that ships hand-crafted samples under examples/*.html (e.g.
live-artifact's stock dashboard, baby health monitor) now lights up one
gallery card per file instead of a single parent card whose preview can
only ever show one of them. The parent stays in the listing tagged
aggregatesExamples=true so findSkillById and Use this prompt still
resolve back to its SKILL.md body, but the Examples tab hides it so the
derived <parent>:<child> cards aren't shadowed by a duplicate preview.
Subfolder layouts (examples/<name>/template.html + data.json) are
deliberately skipped — their templates still hold {{data.x}}
placeholders that only the daemon-side renderer fills in, so showing
the raw template would render visible braces in the gallery. Ship the
baked output as examples/<name>.html alongside the folder to surface it.
Adds an examples.modeLive filter pill (translated across all 21 locales)
that selects skill.scenario === 'live', so refreshable / connector-backed
samples are easy to find without scrolling through every desktop
prototype. live-artifact's SKILL.md gains scenario: live so it (and
every derived card) lights up there.
Co-authored-by: Cursor <cursoragent@cursor.com>
* perf(web): parallelize entry-view bootstrap so each tab renders independently
Bootstrap used to wait on a single Promise.all behind a global
'Loading workspace…' placeholder, which made the slowest endpoint
(typically /api/agents on cold start, since it probes CLI versions)
gate every tab including the ones that don't need agents at all.
Splits the global bootstrapping flag into per-resource loading flags
(agentsLoading, skillsLoading, dsLoading, projectsLoading,
promptTemplatesLoading) plus a daemonConfigLoaded flag for the merged
daemon config. Each tab now blocks only on the data it actually needs:
Examples renders as soon as skills land, Design Systems on dsList,
Designs on projects+skills+designSystems, etc.
Auto-selecting the first available agent and the default design system
moves into dedicated effects gated on daemonConfigLoaded so they no
longer race ahead of the daemon-stored choice and overwrite it with a
freshly picked first-available pick.
EntryView swaps its single loading prop for skillsLoading,
designSystemsLoading, projectsLoading, promptTemplatesLoading so each
inner tab can pick the right gate without leaking the parent's coarse
state.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
9ed4ea1263
|
feat(skill): add github-dashboard (#666)
* feat(skill): add github-dashboard * docs(skill): add github-dashboard screenshot * fix(skill): address github-dashboard review * fix(i18n): cover github-dashboard skill in de/ru/fr fallback lists The localized-content coverage test asserts every skills/<id>/SKILL.md appears in each locale's skills list. Adding github-dashboard to the EN-fallback id list keeps de/ru/fr CI green. * fix(i18n): cover mission-control design system in de/ru/fr fallback lists Same backfill as the one applied on PR #714: mission-control was added in #858 without locale fallback entries, so the localized-content coverage test breaks any open PR once GitHub merges current main into its head ref. --------- Co-authored-by: joey <joey@joeydeMacBook-Air.local> Co-authored-by: lefarcen <935902669@qq.com> |
||
|
|
8b0625aa6f
|
fix(web): unbreak Create button on plain HTTP / LAN-IP deployments (#849) (#900)
`crypto.randomUUID()` is restricted to secure contexts (HTTPS or `localhost`), so when Open Design is served over plain HTTP on a LAN IP — the standard Docker / unRAID / NAS self-hosted setup, e.g. `http://192.168.1.10:17573` — Chromium silently makes the function undefined. Calls then throw `TypeError: crypto.randomUUID is not a function`, which the `try/catch` around `createProject()` swallows by returning `null`, which the click handler reads as "no project, do nothing". The Create button effectively becomes a silent no-op for every LAN-IP user (issue #849, also reported as #394). Centralize the call into a new `apps/web/src/utils/uuid.ts` helper with a three-tier fallback per @lefarcen's review: 1. `crypto.randomUUID()` — secure-context happy path, native and cryptographically random. 2. `crypto.getRandomValues()` + RFC 4122 §4.4 byte layout — still available in non-secure contexts since the Web Crypto API is not gated by `isSecureContext`. Yields a real v4 UUID with crypto-quality entropy. 3. `Math.random()` — last-resort polyfill for environments missing both, kept because the IDs we generate (project ids, message ids, client request ids) are scoped to a single user's local browser session — cryptographic uniqueness isn't required, just enough entropy to avoid collisions. Replace all four `crypto.randomUUID()` callsites confirmed in @lefarcen's audit: - `apps/web/src/state/projects.ts:48` (createProject id) - `apps/web/src/components/ProjectView.tsx:986` (user message id) - `apps/web/src/components/ProjectView.tsx:1013` (assistant message id) - `apps/web/src/components/ProjectView.tsx:1263` (daemon stream clientRequestId) with calls to the new `randomUUID()` helper. Tests: 6 new tests in `apps/web/tests/utils/uuid.test.ts` cover each fallback tier, RFC 4122 v4 format validation (regex + explicit version/variant nibble checks), the explicit "doesn't throw when `crypto.randomUUID` is undefined" assertion that pins the #849 root cause, and a 1000-iteration uniqueness check on the `getRandomValues` path. Verified locally: - web vitest: 522/522 (was 516, +6) - web `tsc -b --noEmit` clean - `tsx scripts/i18n-check.ts` passes |
||
|
|
b06f26a5fd
|
test: strengthen e2e PR coverage (#796)
* test: strengthen e2e PR coverage * fix: address e2e PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * ci: cache Windows packaged smoke builds * test: fake additional agent runtimes * fix: address e2e PR feedback Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Route tools-pack mac starts through a launch-time packaged config override so portable packaged smoke runs keep using the namespace runtime root that inspect and logs expect. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Fall back to the packaged app's embedded config when the build output config is missing so installed mac starts still work. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: align packaged mac PR smoke with tools-pack runtime mode Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Keep blake3-wasm out of the packaged mac daemon prebundle so the standalone runtime loads the Cloudflare asset hasher from node_modules instead of crashing in ESM. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Skip the portable mac launch override when the bundled packaged config is missing so installed fallback app targets can still boot with packaged defaults. Add a regression test covering the missing-config start path. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(pack): remove duplicate mac prebundle dependency key |
||
|
|
8fee22d358
|
Fix stuck chat runs and unintended cancels (#896)
* Fix stuck chat runs and unintended cancels * Harden chat run stall watchdog |
||
|
|
c0c1f6555c
|
add support for VP_HOME environment variable in agent resolution (#859)
* feat: add support for VP_HOME environment variable in agent resolution - Introduced a new .node-version file to specify Node.js version. - Enhanced agent resolution tests to include scenarios for VP_HOME, ensuring proper handling of Vite+ global installs. - Updated platform code to resolve user-scoped home directories, allowing for custom Vite+ installations to be prioritized. - Added tests to verify that the resolution logic correctly honors the VP_HOME environment variable and integrates with existing user toolchain paths. * feat: enhance VP_HOME support in sidecars and platform - Updated the PACKAGED_CHILD_ENV_ALLOWLIST to include VP_HOME for environment variable forwarding. - Exported functions resolvePackagedChildBaseEnv and resolvePackagedPathEnv for better accessibility in tests. - Added tests to validate VP_HOME handling in packaged child environments and ensure correct path resolution. - Adjusted wellKnownUserToolchainBins to prioritize VP_HOME/bin in the toolchain path resolution. |
||
|
|
e14b8092ea
|
feat: add Orbit activity summaries (#681)
* feat: add Orbit activity summaries * fix(orbit): make runs navigable while agent continues * fix(web): widen minimum chat panel * feat: support Orbit template selection * fix(daemon): avoid bogus skill side-file preflight * fix(web): collapse orbit artifact project cards * fix(web): preserve orbit project card titles * fix: improve Orbit run daily briefing * fix: handle Orbit digest data failures * fix: load Orbit templates and connector tools reliably * fix: keep Orbit summary counts consistent Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: apply Orbit template skill context * fix: cache and curate connector tools for Orbit * fix: align Orbit defaults and connector discovery * fix: simplify Orbit template settings * fix: move connectors into settings * fix: compact connector settings catalog * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: prevent connector action button from stretching into pill The icon-only connect/disconnect buttons in the embedded connectors catalog inherited min-width: 92px / 106px from the non-embedded pill rules, overriding the 24px square sizing and causing the buttons to overlap the card head text. Reset min-width to 0 in the embedded icon-only rule so the compact square layout holds. * fix(web): align live artifact file rows * fix: clean up Orbit connector settings lifecycle Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address Orbit review regressions Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * feat(web): localize Orbit and connector settings * feat(web): gate Orbit runs without connectors * feat(web): refine connector settings UX * feat(web): safeguard Composio key clearing * fix(web): refresh Composio tool badges * feat(web): show connector logos * feat(daemon): localize Orbit prompt window * fix(daemon): clarify blocked connector callback closes * test(daemon): harden flaky async probes * fix(web): align Indonesian connector locale keys * test(web): align connector browser props * fix(web): preserve explicit credential clears Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): time out Composio logo proxy fetches Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): localize Indonesian connector settings copy Translate the new connector settings strings in the Indonesian locale and lock them with a regression test so this surface no longer silently falls back to English. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): preserve discovered connector tools Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): preserve onboarding autosave completion Keep settings autosave from clearing onboarding completion after the close gesture, and expose the desktop main types from source so workspace validation can typecheck packaged imports without a prior desktop build. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): defer Composio catalog cache hydration Load persisted Composio catalog data only after the runtime data directory is configured so startup cannot read another namespace's cache. Add a regression test that exercises the module-load singleton path. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): treat discovery completion independently Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): preserve latest settings draft on close Use the latest persisted settings draft when the dialog closes so onboarding completion does not race a stale daemon sync and overwrite newer Orbit/template selections. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): avoid syncing draft Composio key on Orbit run Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): localize Orbit settings copy Translate the new Indonesian Orbit and autosave strings so the settings UI no longer falls back to English and the locale regression stays covered. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): prefer fresh connector catalog state Keep refetched connector status/auth data authoritative while retaining discovery-only tool metadata so the connectors UI stays consistent after refreshes. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): declare Indonesian locale fallback keys explicitly Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): inline Indonesian fallback strings for CI Replace the Indonesian locale's per-key English lookups with explicit strings so workspace typecheck no longer depends on brittle build-mode resolution in CI. Add a regression test that blocks those per-key English lookups from reappearing in the CI-sensitive fallback sections. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): restrict proxied connector logos to image MIME types Reject non-image upstream logo responses so the daemon never serves third-party HTML from its localhost origin. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * test(e2e): align settings dialog regressions Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): decouple Orbit runs from media sync failures Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): keep SPA catch-all export-compatible Disable dynamic catch-all params for the exported SPA shell so Next.js static builds can emit the root route again. Add a regression test covering the route config against the web export mode. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): preserve Orbit config and workspace routes Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): block SVG in connector logo proxy Reject SVG and other unsafe proxied logo responses so third-party logo content cannot execute under the daemon origin, while keeping raster logo fetches working and making rejected responses non-cacheable. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): fall back to static catalog for empty cache Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): disable Orbit run before connector gate resolves Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(desktop): export shipped desktop types Point the desktop ./main type export at the generated declaration so installed consumers resolve the published file set. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): restore persisted question form selections Render historical submitted answers directly so reloaded question forms keep their locked selections visible. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): retry forced media sync autosave Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): keep Composio logo timeout through body read Keep the Composio logo fetch timeout active until the response body is fully consumed so stalled body reads abort and clear the inflight cache entry. Add a regression test that proves a delayed body read times out and the next request can recover.\n\nGenerated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): refresh Orbit gate after connector auth Re-check connector availability when the settings window regains focus so Orbit unlocks as soon as a connector finishes authenticating in the same settings session. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): keep connector detail tool lists intact Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): ignore malformed Orbit summaries Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(e2e): stabilize design-system multi-select flow Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): cap Composio logo cache growth Bound the Composio logo cache with LRU eviction and expired-entry pruning so repeated untrusted logo requests cannot grow daemon memory without limit. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): bound proxied Composio logo payloads Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): align autosave settings tests Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): remove stray CSS conflict marker Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fixer: address PR #681 follow-up items Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): restore restart routes and connector flows * fix(web): keep SPA export route static * fix(web): stabilize chat scroll tests --------- Co-authored-by: lefarcen <935902669@qq.com> |
||
|
|
aec9428b08
|
Fix desktop preview and packaged app interactions (#879)
* Fix packaged deck navigation interactions * Fix connector auth in packaged app and localized content coverage * Fix Electron connector browser handoff contract |
||
|
|
b9d30aa30e
|
test(web): de-flake chat-scroll-preservation across tab switches (#886)
The earlier shape installed instance-level Object.defineProperty mocks
on the *remounted* chat-log only after `await switchTab('Chat')`. Inside
that act() the component schedules a rAF that writes scrollTop on the
new element; depending on whether jsdom's rAF polyfill flushed before
the await resolved, the write either landed on the still-default
prototype setter (lost) or the not-yet-installed instance setter (also
lost). The instance mock's closure-captured remountedTop then served
its initial 0 forever and the assertion failed nondeterministically
across CI runs without any product-code change.
Patch the geometry at HTMLElement.prototype level so any chat-log
React mounts later automatically reads/writes through a
test-controlled `geom` object. The component's restore rAF can fire
at any point and still write to the same place the assertion reads
from. Verified 8/8 clean local runs.
|
||
|
|
c2e8fc3b02
|
feat(design-systems): add Urdu Modern (Indus Script) system (#714)
* feat: add Urdu Modern design system * fix: address review comments (font mismatch, contrast, and i18n fallback) * fix: address all review comments for Urdu design system * fix: resolve i18n crash, font URL mismatch, and markdown syntax error * fix: remove font URL space and update Quick Start font token * docs: fix quick start link syntax and icon source order * fix(i18n): cover urdu design system in de/ru/fr locale dictionaries The localized-content coverage test scans design-systems/*/DESIGN.md and asserts every id appears in each locale's designSystems list, and every `> Category:` value is a key in designSystemCategories. Adding the new `urdu` design system without these entries breaks de/ru/fr CI. Add urdu to the EN-fallback id list and translate the new 'Editorial / Personal / Publication' category for all three locales. * fix(i18n): cover mission-control design system in de/ru/fr fallback lists Mission Control was added in #858 but its design-system id was never added to the locale fallback arrays, so the localized-content coverage test breaks once main and any open PR share the same merge ref. --------- Co-authored-by: unknown <muhammadanas0261@gmail.com> Co-authored-by: lefarcen <935902669@qq.com> |
||
|
|
47a014d377
|
Add BMW M design system (#579)
* Add BMW M design system * Address BMW M design system review feedback * Fix BMW M palette swatch parsing |
||
|
|
661d11e60b
|
fix(web): confirm before clearing the saved Composio API key (#877)
The Clear button on Settings → Connectors removed the daemon-stored Composio key in a single click with no recovery — a stray click wiped a credential the user had to fetch back from app.composio.dev. Wrap the existing onClick in window.confirm() matching the same pattern the codebase already uses for destructive actions (conversation delete, design delete, FileWorkspace file delete, and the Media providers Clear button shipped alongside this in issue #737). The prompt copy stays in English to match the rest of the Composio section, which is hardcoded English today. Updated the existing 'clears a saved Composio key' test to auto-accept the prompt, plus added a sibling test asserting that dismissing the prompt leaves the daemon-stored key intact in the saved payload. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
0c383af332
|
add trading analysis dashboard template skill (live artifacts) (#824)
* add trading analysis dashboard template skill for live artifacts Package the Wall-Street-style dashboard as a template-mode skill with a default example, checklist, and seed template, and register i18n fallback coverage for the new skill id. * fix(skill): address P1 chart axis labels, units, and legends lefarcen review: - checklist required >=3 charts with axis labels, units, and legends, but template ships with 2 charts and neither had labels/units/legends. - Adjusted checklist P0 to require >=2 charts (matches actual template) and kept the axis-label / unit / legend sub-gates. - Annotated both Option Greeks and Cumulative PnL charts with x-axis tick labels, y-axis tick labels, axis titles with explicit units (Strike $, Sensitivity Δ/Γ, Session Time ET, Equity $ USD), and a legend row naming each plotted series. Charts now satisfy the P0 gate. * fix(skill): make localStorage access safe inside sandboxed preview iframes mrcfps Looper review: Open Design renders HTML artifacts in sandboxed iframes that allow scripts but not same-origin access. The template's top-level localStorage.getItem / setItem calls could throw SecurityError before demo, theme, palette, and chart handlers were registered, leaving the artifact static in the primary preview path. Wrap reads/writes in safeGetTheme / safeSetTheme helpers that swallow SecurityError so the document can still apply the active theme on documentElement and continue initializing interactive handlers when storage is unavailable. Persistence becomes best-effort, interactivity becomes guaranteed. * fix(skill): align Option Greeks x-axis tick labels with strike circles lefarcen review (a346e80 regression): The axis ticks added in a346e80 placed 145/150/155/160/165 at x=40/120/200/300/380, but the linked .strike-150/-155/-160 circles already sit at cx=200/300/380. With the previous labels, hovering Option Chain row data-strike=150 would highlight the chart point at x=200, where the axis read 155. The label-versus-data hover link was visually inconsistent. Shift the axis tick labels to 140/145/150/155/160 at x=40/120/200/300/380 so strike 150/155/160 labels sit directly under their circles, restoring the table↔chart hover-link semantics. --------- Co-authored-by: tuolaji <tuola@tuolajideMacBook-Air.local> Co-authored-by: Tuola Ge <gexingli@refly.ai> |
||
|
|
2df8b775ec
|
feat(skills): add 32 zhangzara HTML deck templates (#704)
* feat(skills): add 32 zhangzara HTML deck templates Vendored from upstream MIT-licensed zarazhangrui/beautiful-html-templates — one Open Design skill per template (name prefix `html-ppt-zhangzara-`) so each template surfaces as its own entry in the Examples panel and renders its own preview. Each skill ships: - SKILL.md (frontmatter + workflow), description, triggers, and od.upstream pointing at the source folder - example.html (the self-contained deck; daemon's preview route looks for <skillDir>/example.html) - template.json (upstream metadata snapshot, with `slug` re-prefixed to `zhangzara-<base>` and a `source` URL) - assets/deck-stage.js / assets/styles.css for the 8 templates that ship a runtime; HTML refs rewritten so the daemon's iframe URL rewriter resolves them through /api/skills/<id>/assets/ scripts/guard.ts allowlist updated with the `html-ppt-zhangzara-` prefix so the vendored upstream JS runtimes pass the residual-JS check. * fix(skills, i18n): address PR #704 review feedback - Add the 32 new html-ppt-zhangzara-* skill ids to the de/ru/fr SKILL_IDS_WITH_EN_FALLBACK arrays so the localized-content coverage e2e test passes. The vendored upstream templates are English-only; falling back to the upstream English description is the right semantic for this batch. - Also add the pre-existing social-media-dashboard skill and totality-festival design system to the same fallback arrays (introduced in #678 without i18n coverage). Tagged with TODOs so localized copy can land in a follow-up. - Ship the upstream MIT LICENSE file in each skills/html-ppt-zhangzara-*/ folder so the copyright/permission notice travels with the vendored copy, as MIT requires for redistributing substantial portions. Update each SKILL.md's Source section to reference the bundled LICENSE. - For the 8 runtime-backed templates (creative-mode, editorial-tri-tone, neo-grid-bold, peoples-platform, pin-and-paper, pink-script, soft-editorial, stencil-tablet), expand the workflow's clone step to instruct the agent to copy the assets/ folder alongside example.html — the skill HTML references assets/deck-stage.js (and assets/styles.css for pin-and-paper) as project-local paths, so cloning the HTML alone produces an artifact whose runtime 404s. Verified locally: - pnpm guard passes. - pnpm --filter @open-design/web typecheck passes. - pnpm --filter @open-design/web test passes (309/309). - pnpm --filter @open-design/e2e test passes (6/6 active, including localized-content coverage for de/ru/fr). * fix(i18n): drop duplicate totality-festival fallback after merge with main Main already added 'totality-festival' to the design-system EN-fallback lists; the TODO entry from this branch became a duplicate after merge. * fix(skills, guard): address PR #704 follow-up review - Pin Chart.js CDN to 4.4.7 in coral and cartesian example.html so vendored decks no longer track the latest jsDelivr major. - Narrow scripts/guard.ts zhangzara allowlist to a regex that only permits skills/html-ppt-zhangzara-*/assets/deck-stage.js, restoring the TypeScript-first guard for any other JS under those skill dirs. - Reconcile slide_count and 'Slides in demo' with actual <section class="slide"> counts: broadside 20 -> 16, monochrome 18 -> 16, neo-grid-bold 13 -> 12. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(daemon): keep resolveDataDir return path stable, canonicalize at compare site The realpathSync wrapper inside resolveDataDir was rewriting every /var/... result to /private/var/... on macOS, which broke 11 hermetic assertions in tests/resolve-data-dir.test.ts (absolute paths, relative paths, and \$HOME / \${HOME} / ~ expansions whose mkdtempSync roots live under /var/folders/...). It also changed the public OD_DATA_DIR resolution contract for any downstream caller that compared against the expanded user-supplied path. Restore resolveDataDir to return the expanded resolved path unchanged, and introduce RUNTIME_DATA_DIR_CANONICAL — a one-shot realpath of RUNTIME_DATA_DIR — used only at the narrow folder-import comparison site that needs to match against a user-supplied realpath() result. The import-path symlink protection from #624 still works (a /var-rooted data dir now compares against its /private/var canonical form), while resolveDataDir keeps its stable, user-shaped contract. Verified locally: pnpm --filter @open-design/daemon test (1083/1083), including all 12 resolve-data-dir.test.ts cases. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
369d136d19
|
Add Docker Compose deployment workflow (#65)
* Add Docker Compose deployment workflow * Address Docker deployment review feedback Harden publishing inputs and temporary credential handling, and tighten Docker runtime defaults requested by the PR review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix Docker publish build in CI mode Set CI=true during the image build so pnpm prune can run non-interactively inside Docker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix Docker runtime dependency layout Use pnpm deploy for the daemon package so the runtime image includes production dependencies where Node resolves them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Use legacy pnpm deploy in Docker build Allow pnpm v10 deploy to package the daemon workspace without requiring injected workspace packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Align Docker runtime with Node 24 Use Node 24 for both build and runtime stages and update image verification for the workspace daemon dependency layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Remove legacy OD_HOST Docker binding fallback Use OD_BIND_HOST as the single daemon bind-host setting for Docker deployment and origin validation. * Update Docker image verifier for daemon dist runtime Check the packaged daemon dist entrypoint and allow npm from the Node 24 runtime image while still rejecting build-only tools. * Allow private LAN browser origins for daemon * Share daemon origin validation helpers Move browser origin validation into a shared daemon module so tests exercise the production logic and cover the remaining private LAN edge cases. * Harden Docker Compose port exposure Bind the Compose deployment to localhost by default and pass the published port through to the daemon origin checks so host-port overrides remain same-origin. * Keep deployment hosts out of local-only no-origin checks Require an actual matching Origin before configured deployment origins can satisfy local-only daemon guards, preventing no-Origin remote clients from bypassing those checks. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: mrcfps <mrc@powerformer.com> Co-authored-by: lefarcen <935902669@qq.com> |
||
|
|
8930b9650c
|
feat: Add a toggle to reveal media provider API keys (#867) | ||
|
|
665e52b295
|
fix(daemon): pin OD_DATA_DIR in /api/mcp/install-info env so the macOS-packaged MCP server does not EPERM on .od/projects (#857)
* fix(daemon): pin OD_DATA_DIR in /api/mcp/install-info env so spawned MCP processes do not fall back to .od inside the macOS app bundle Reporter (#848) ran a packaged Open Design 0.5.0 on macOS and pointed Antigravity's MCP config at the bundle's daemon-cli.mjs. The MCP process is launched by the IDE outside the packaged app's environment, so it does not inherit OD_DATA_DIR. The daemon-cli import path runs mkdirSync('<cwd>/.od/projects') before dispatching to MCP mode, and <cwd> resolves to the read-only macOS app bundle, hitting EPERM. The /api/mcp/install-info endpoint already serializes env into every client snippet (Cursor, Claude Code, VS Code, Zed, Windsurf, Antigravity, Codex). Add OD_DATA_DIR: RUNTIME_DATA_DIR to that env so the snippet pins the daemon's resolved data root, and the spawned MCP process writes to the same directory the daemon already uses regardless of how the IDE was launched. Test added asserts env.OD_DATA_DIR is propagated. * refactor(daemon): extract buildMcpInstallPayload so the test asserts the production helper, not a fixture mirror Reviewer flagged that the previous test asserted env.OD_DATA_DIR on a copy of the handler's payload-construction logic, which would silently pass if the real handler ever diverged from the fixture. Move the env / args / buildHint shape into a pure exported helper (apps/daemon/src/mcp-install-info.ts), wire both server.ts and the test fixture through it, and drop the inline duplicates. The test now exercises the same code path that ships, so any regression in the env block (missing OD_DATA_DIR, wrong format, lost ELECTRON_RUN_AS_NODE) fails it. --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
c55d058024
|
fix(web): differentiate recent and your designs sorting (#845)
* fix(web): differentiate recent and your designs sorting * fix(web): remove the immediate return statement from sorting logic * fix(web): add sorting your design tab by creation time * fix(web): update card timestamps * fix(web): align sort keys and timestamps across designs tab views |
||
|
|
6de802ba70
|
feat(daemon): add critique interrupt endpoint + project-keyed run registry (Task 6.1) (#819)
Phase 6.1 of the Critique Theater rollout: a single new endpoint and the in-process registry that backs it. POST /api/projects/:projectId/critique/:runId/interrupt cascades an AbortController to the orchestrator that owns the spawned CLI so the parser can flush best-so-far state and emit critique.interrupted before the process exits. Backed by a new in-process run registry that the orchestrator wiring registers each run into before runOrchestrator is invoked, and unregisters in a finally block. The registry is keyed by (projectId, runId), not just runId. A request to interrupt project p1's runId cannot find or abort a registry handle that belongs to project p2 even if their ids ever collide. The HTTP handler also performs its own DB-row projectId check before calling the registry, so cross-project leakage is blocked at two layers. The endpoint is idempotent on already-interrupted rows: a client that lost the first response and retries observes 202 with prevStatus "interrupted" rather than a 409 conflict. Other terminal statuses (shipped, failed, timed_out, degraded, below_threshold, legacy) still return 409 because those runs reached their real terminal state on their own and an interrupt is no longer meaningful. Recovery path for stale running rows: when registry.interrupt returns false (the in-process registry has no AbortController for this projectId/runId pair) but the DB still says 'running', the endpoint marks the row 'interrupted' directly with recoveryReason='no_live_handle' and returns 202 with recovered=true. This window opens after a daemon restart in the gap before reconcileStaleRuns sees the row old enough. Without the recovery branch the endpoint would lie: 202 accepted, no child signaled, no critique.interrupted event, row stuck running. The new persistence helper markRunInterruptedRecovery mirrors the per-row write reconcileStaleRuns already does, gated on status='running' so a row that just transitioned terminal is not overwritten. Task 6.2 (rerun endpoint) is intentionally not in this PR. The earlier draft conflated row insertion between the handler and runOrchestrator (primary key collision) and did not actually start a new agent spawn. Rerun needs a real chat-run path with prior-art context, an artifact-id validator, and SQL LIKE escaping that the row lookup path is missing today; it is cleaner shipped as a follow-up than wedged into this PR. Tests: - critique-run-registry: 17 cases covering register, get, interrupt, unregister, list, plus the new (projectId, runId) composite key invariants (cross-project register, cross-project get/interrupt isolation, unregister keying). - critique-interrupt-endpoint: 17 cases covering 202 happy path, 404 on unknown run, 404 on cross-project run, 404 cross-project leak guard at the registry layer, 409 on terminal statuses, 202 idempotent retry on already-interrupted, stale-handle defense, 202 + recovered on a stale running row with no live handle, 400 on bad params. Incidental: apps/web/src/i18n/locales/id.ts was missing 18 fileViewer deploy/Cloudflare keys after upstream landed PR #805 (R2 release publishing). Without those keys the workspace web typecheck fails on the i18n Dict equality check, blocking CI on every PR. Added Indonesian translations for the missing keys to unblock. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
e52720aa12
|
feat(daemon): add language boost support for Minimax TTS (#773)
* feat(daemon): add language boost support for Minimax TTS Add --language CLI flag to support language boost parameter for Minimax TTS. This enables better pronunciation for specific languages like Cantonese (Yue). * docs(media): add --language flag to media generation contract Document the language boost parameter for Minimax TTS, enabling better pronunciation for specific languages like Cantonese (Yue). * fix(media): correct Cantonese language_boost value and add input validation - Use correct MiniMax value 'Chinese,Yue' for Cantonese (no space) - Add type guard in server.ts to reject non-string language values - Trim language string before sending to MiniMax API --------- Co-authored-by: root <root@DELLN40.asiacredit.org> |
||
|
|
655d561f38
|
fix(web): show explicit error/retry state when example preview HTML fails to load (#863)
* fix(web): show explicit error/retry state when example preview HTML fails to load Reporter (#860) saw the example preview modal stuck with the toolbar buttons greyed out and only restarting the app got back to a usable state. Lefarcen confirmed the diagnosis: when /api/skills/:id/example fails, fetchSkillExample returns null, the modal stays at preview.loading forever, and the share menu's disabled={!activeHtml} guard sits in the disabled position with no recovery path. Three changes: 1. fetchSkillExample now returns a discriminated { html } | { error } instead of collapsing every failure into null, so callers can tell a real fetch failure from a normal load. 2. PreviewView gains an optional error field. When set, PreviewModal renders a stacked title/body/Retry affordance instead of the indefinite "Loading…" placeholder. Retry re-fires onView so the parent can re-run its fetch. 3. ExamplesTab tracks per-skill errors alongside per-skill html, clears the in-flight value before each fetch, and wires onView from the modal into loadPreview so the Retry button actually retries. i18n: three new keys (preview.errorTitle, preview.errorBody, preview.retry), translated across all 17 locales. The locales-aligned test stays green. CSS: .ds-modal-error stacks the new content vertically inside the existing .ds-modal-empty positioning, no other modals affected. * fix(web): stabilize preview onView and guard parallel preview fetches Codex caught a real bug in the round-1 fix: the inline onView={() => loadPreview(...)} prop was recreated on every parent render, and PreviewModal's mount effect re-fires onView whenever its identity changes. A persistent fetch failure would update state, recreate the prop, re-fire the effect, re-run loadPreview, and burn through the error UI in a flash instead of waiting for a Retry click. Pin a stable onPreviewView via a useRef-backed callback so the modal sees a single identity for the lifetime of the panel; loadPreview is reached through the ref, so its closure refresh on state updates no longer leaks into the modal's effect dependencies. While in this surface, also add lefarcen's race guard: a synchronous inFlightRef Set so two parallel loadPreview calls (e.g. card hover firing while the modal opens) cannot both pass the cache check before either setState lands. The first caller adds the id pre-await; the second sees it and exits early. try/finally clears the entry on both success and failure paths. Adds tests/components/preview-modal-error-state.test.tsx covering: - error UI renders when view.error is set, - Retry click calls onView with the active view id, - re-rendering with the same onView identity does not re-fire the modal's mount effect (pins the no-auto-retry contract). * fix(web): close Retry over the active skill id, not the modal-internal view id mrcfps caught a real regression in round 2: PreviewModal calls onView(activeId) where activeId is the modal-local view id ('preview' in this component). The previous round forwarded that argument straight into loadPreview, so the mount effect and Retry button hit /api/skills/preview/example instead of /api/skills/{skill-id}/example. The new error state could not actually recover. Mirror the active skill id into a ref alongside loadPreviewRef and have onPreviewView ignore the modal-forwarded argument, fetching the selected skill via the ref instead. The callback identity stays stable, so the no-auto-retry contract from round 2 still holds. Adds tests/components/examples-tab-retry.test.tsx that mounts the real ExamplesTab, mocks fetchSkillExample to reject, opens the preview, clicks Retry, and asserts the second call hits the same skill id (and explicitly never gets called with 'preview'). --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
2eae7da24b
|
feat: support Cloudflare Pages custom domains (#851)
* Support Cloudflare Pages custom domains without hiding pages.dev fallback Keep the default Pages preview as the first public link while optional owned-zone binding provisions DNS and Pages custom-domain state in parallel. Constraint: Cloudflare deploys must use the existing direct-upload API path with no Wrangler dependency. Constraint: pages.dev must stay visible even while custom-domain verification is pending. Rejected: Vercel custom-domain support | outside requested Cloudflare-only scope. Rejected: overwriting arbitrary CNAME records | risks taking over user-managed DNS. Confidence: high Scope-risk: moderate Directive: Do not expose providerMetadata through public deploy contracts; keep custom-domain DNS ownership checks conservative. Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts Tested: pnpm --filter @open-design/contracts build && pnpm --filter @open-design/contracts typecheck && pnpm --filter @open-design/contracts test Tested: pnpm --filter @open-design/web typecheck && pnpm --filter @open-design/web test -- providers/registry.test.ts components/FileViewer.test.tsx i18n/locales.test.ts Tested: pnpm i18n:check && pnpm guard && pnpm typecheck Tested: pnpm --filter @open-design/daemon build && pnpm --filter @open-design/web build && git diff --check Not-tested: real Cloudflare account/token/domain smoke test * Preserve Cloudflare fallback correctness under large accounts and races Constraint: Cloudflare Pages keeps pages.dev as the primary usable fallback while custom domains remain optional typed metadata. Rejected: Treating custom-domain DNS or binding failure as a top-level deployment failure | pages.dev can still be ready and usable. Confidence: high Scope-risk: moderate Directive: Keep custom-domain finality tied to Cloudflare Pages API active status plus URL reachability; do not expose providerMetadata. Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts; pnpm --filter @open-design/web test -- components/FileViewer.test.tsx i18n/locales.test.ts providers/registry.test.ts; pnpm --filter @open-design/daemon typecheck; pnpm --filter @open-design/web typecheck; pnpm i18n:check; git diff --check; pnpm guard; pnpm typecheck; pnpm --filter @open-design/daemon build; pnpm --filter @open-design/web build Not-tested: Real Cloudflare token/account/zone smoke test. * Keep impeccable design notes local Constraint: .impeccable.md is local assistant/design context and should not be part of the PR diff. Rejected: Keeping the file tracked while adding it to .gitignore | tracked files are not ignored by Git. Confidence: high Scope-risk: narrow Directive: Keep .impeccable.md untracked and ignored; do not rely on it for required project documentation. Tested: git check-ignore -v .impeccable.md; git diff --check Not-tested: Full workspace tests not rerun for ignore-only metadata change. |
||
|
|
77824ec029
|
fix(web): preserve Chat scroll position across Chat/Comments tab switches (#790) (#841)
* fix(web): preserve Chat scroll position across Chat/Comments tab switches (#790) The chat-log <div> in ChatPane is conditionally rendered (the inner `{tab === 'chat' ? <>...</> : null}` branch). When the user switches to Comments and back, the chat-log is unmounted and remounted; the remounted element starts at scrollTop=0, and the initial-bottom-scroll effect skips because didInitialScrollRef.current is already true from the original mount. Result: the conversation view jumps to the top instead of preserving the user's reading position. Replaced the empty-deps scroll listener with a tab-keyed effect that: 1. Captures scrollTop in the existing onScroll handler so the saved position is always current. 2. On every mount of the chat-log (when tab becomes 'chat'), restores the saved scrollTop on the next animation frame so layout finishes before the scroll write lands. The existing scrolledFromBottom signal that drives the jump-to-bottom button is folded into the same handler and now correctly re-attaches on every chat-log remount, fixing a secondary issue where that listener would silently stop firing after a tab toggle. * fix(web): preserve bottom-pinned chat across off-tab streaming and snapshot on unmount Round 1 saved an absolute scrollTop, so a user who left Chat while pinned to the bottom came back above any new messages that streamed in while Comments was open. Save a discriminated state instead: { pinnedToBottom: true } when the user was within 50px of the bottom, otherwise { scrollTop }. On remount, pinned state snaps to the new scrollHeight so bottom-followers stay pinned; non-pinned state restores the absolute offset. Also snapshot the final scroll state in the effect cleanup before removing the listener, so programmatic scrolls or layout shifts right before unmount don't leave the ref stale. Adds tests/components/chat-scroll-preservation.test.tsx covering both branches. * fix(web): clear saved chat scroll state on conversation switch The savedChatScrollRef persisted across conversation changes, so switching to Comments while on conversation A and then switching to conversation B would, on returning to Chat, restore A's scrollTop instead of starting fresh at the bottom. Reset the ref alongside didInitialScrollRef when activeConversationId changes. Added a third test covering the cross-conversation case. * fix(web): scroll new conversation to its bottom when conv switch happened off-tab When activeConversationId changed while the user was on the Comments tab, the conversation-reset effect cleared didInitialScrollRef and the saved scroll ref, but the initial-bottom-scroll effect couldn't do anything because logRef.current was null. Returning to Chat then left the new conversation at scrollTop: 0 instead of its initial bottom. Add `tab` to the initial-scroll deps so the effect re-runs when the chat-log remounts, picks up the cleared didInitialScrollRef state, and scrolls the fresh conversation to its scrollHeight. Updated the cross-conversation test to assert the new conversation lands at its bottom (1000), not at scrollTop: 0. * fix(web): resync jump-to-latest button when restoring saved chat scroll position The rAF restore branch wrote scrollTop but never refreshed scrolledFromBottom, so a user who left Chat ~60px from the bottom and returned to find new messages stacked underneath would land hundreds of pixels above the latest turn while the jump-to-latest button stayed hidden until they manually scrolled. Recompute the distance and update scrolledFromBottom inside the restore rAF, mirroring what onScroll already does. Adds a test that asserts the jump-to-latest button is visible immediately after a non-pinned restore over a grown scrollHeight. --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
11225b2d7e
|
fix(desktop): cleanly quit macOS packaged app (#422)
Co-authored-by: Fu Yizheng <fyz3120@sina.cn> |
||
|
|
604d3660f2
|
fix(web): reserve clearance for MCP Copy button so it stops overlapping the snippet (#847)
* fix(web): reserve clearance for the MCP Copy button so it stops overlapping the snippet The Copy button is absolutely positioned at top: 8 right: 8 over the snippet <pre>, but the <pre> only had padding: 12px 14px so the first line of the command sat directly under the button. Wrapped bash one- liners also reached the right edge and continued behind it. Reserve the clearance in the <pre>'s own padding instead of moving the button: padding: '40px 80px 12px 14px' keeps the button anchored where it is, lets the first line render below it, and stops a wrapped one-liner short of the button column. Closes #632 * fix(web): bump MCP snippet right padding to clear the wider Copied state Reviewer pointed out 80px right clearance can be tight at elevated font sizes / zoom: the post-click Copied state (icon + text + button padding + 8px right offset) reaches close to that limit. Bump to 104px so there's a comfortable buffer in either button state. --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
32df17b87b
|
Fix desktop preview interactions and connector auth feedback (#864)
* Fix desktop preview modal interactions * Fix connector auth failures surfacing |
||
|
|
959bfaa817
|
fix(daemon): make MCP install snippet survive daemon port changes (#846)
* fix(daemon): make MCP install snippet survive daemon port changes `od mcp` now discovers the live daemon URL via the sidecar IPC status socket on every spawn, so the Settings -> MCP server snippet no longer bakes in `--daemon-url <port>`. Pasted client configs stay valid across daemon restarts even when the daemon binds an ephemeral port (tools-dev, packaged). Resolution order is --daemon-url > OD_DAEMON_URL > IPC discovery > http://127.0.0.1:7456 so explicit overrides still win for direct `od` launches. * fix(daemon): MCP snippet works in non-default namespaces and direct launches Propagate OD_SIDECAR_NAMESPACE / OD_SIDECAR_IPC_BASE into the snippet env so non-default namespace daemons stay reachable; the spawned MCP client does not inherit the daemon's env, so without this it would probe the default-namespace socket and miss. Restore --daemon-url in the snippet for direct `od --port X` launches that have no IPC socket. Reword `od mcp --help` so it does not imply live URL tracking; each new spawn rediscovers, but a running MCP server caches the URL until the client restarts. |
||
|
|
fa63278b84
|
fix(web): give MCP server Copy button a solid surface so it reads against the code block (#742) (#840)
* fix(web): give MCP server Copy button a solid surface so it reads against the code block (#742) The Copy button in the MCP server section is positioned absolute over a syntax-highlighted <pre> code block. button.ghost's default background: transparent let the dark code surface bleed through, so on some themes the button rendered nearly invisible against the snippet backdrop. Users could miss the primary copy affordance entirely. Pinned background: var(--bg-panel), an explicit border, and a small shadow to the inline style so the button floats as a visible chip above the code block in both light and dark themes. Hover/disabled behavior remains delegated to the existing .ghost class rules so the visual contract elsewhere in the app stays unchanged. * fix(web): move MCP Copy button surface to a CSS class so hover still works Previous round set background and border inline on the button, which overrode button.ghost:hover:not(:disabled) from index.css and silently killed the hover state change. Move the solid panel background, border, and shadow into a scoped .mcp-copy-btn class with its own :hover:not(:disabled) rule, and keep only positioning inline. --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
6b117913b9
|
fix(web): truncate long inspect-panel labels so they cannot spill past the panel edge (#780) (#838)
* fix(web): truncate long inspect-panel labels so they cannot spill past the panel edge (#780) The picker's inspect panel renders the selected component's label as a <strong> inside .inspect-panel-title. The grid container had min-width: 0 so it could shrink, but the inner <strong> rule only set font-size with no overflow constraints. A deeply-nested component with a long generated selector path produced a label longer than the 296px panel width, and the text spilled out past the panel's right edge instead of clipping inside the title's background frame. Added white-space: nowrap + overflow: hidden + text-overflow: ellipsis on .inspect-panel-title strong so the label truncates within the panel boundary. The full string remains accessible to users via the title attribute already present on the sibling <code> element that renders the same selector context. * fix(web): expose full inspect-panel label via title attribute on truncated <strong> Reviewer flagged the comment claiming the full label was accessible via the sibling <code>'s title — but that <code> carries target.selector, not target.label. Add title={target.label || target.elementId} to the <strong> itself so the truncated label is recoverable on hover, and align the CSS comment. --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
b3259f5baa
|
fix(web): keep Design Files tab visible when workspace tabs scroll (#842)
When many tabs open, the tab strip scrolls horizontally and the Design Files entry slides off the left edge, leaving no obvious way back to it. Pin the Design Files button with position: sticky and a small shadow so it stays anchored at the left while the rest of the strip scrolls behind it. Closes #775 Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
42bcfb6561
|
fix(web): keep inspect-panel close button on a stable single-line layout (#785) (#839)
The .inspect-panel-head row laid out a flexible title block next to the Close button with display: flex and gap: 10px, but no shrink ceiling on the button. When the selected component had a long generated selector, the title block consumed almost all available width and the button shrank below its natural glyph width. On some font/zoom combinations the single-character label rendered stacked vertically rather than as a normal horizontal control. Pinned flex-shrink: 0 on .inspect-panel-head > button so the close control reserves its natural size regardless of how much the title expands. The button stays on a single horizontal line for any selector length the panel can render. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
8bb9900603
|
fix(web): scope settings save validation + sanitize payload to active sidebar section (#739) (#827)
The footer Save button's enabled state was computed purely from execution-mode completeness (BYOK requires apiKey + model + valid baseUrl; Local CLI requires a selected available agent). That check ran regardless of which sidebar section the user was on, so a draft mode toggle on the execution section that left required fields empty would lock the Save button across every other section. After clicking BYOK without filling fields and navigating to Language or Appearance, the user could not save unrelated changes in those sections even though they had nothing to do with execution mode. Two paired helpers in apps/web/src/components/SettingsDialog.tsx address this: shouldEnableSettingsSave(cfg, activeSection, agents, isBaseUrlValid) returns true on any section other than 'execution' so unrelated sections do not get blocked by an incomplete execution draft. On 'execution' it keeps the original mode-completeness check unchanged (within-section invariant). sanitizeSettingsSavePayload(cfg, initial, activeSection, agents, isBaseUrlValid) is the counterpart used at the onSave call site. When Save is enabled on a non-execution section but the user's draft execution config is incomplete, it reverts the execution-mode fields (mode, apiKey, apiProtocol, apiVersion, apiProtocolConfigs, apiProviderBaseUrl, baseUrl, model, agentId, agentCliEnv, maxTokens) to their `initial` values so the unrelated section change is committed without leaving the app in a broken execution state. Within the execution section, or when execution is already valid, the cfg passes through unchanged. Both lefarcen and chatgpt-codex flagged this persistence gap on the first revision of this PR; mrcfps marked it blocking. The sanitize helper is the fix lefarcen suggested (revert-to-initial when the active section is not execution and the execution draft is incomplete). Tests in apps/web/tests/components/SettingsDialog.test.ts: - shouldEnableSettingsSave: 4 cases (the cross-section fix, daemon mode validity, api mode validity, regression guard for within-execution). - sanitizeSettingsSavePayload: 5 cases (revert path, no-op when execution is valid, no-op on the execution section itself, every non-execution section covered, edge case where the agent registry says unavailable but initial cfg was already valid daemon). Local: web tests 33/33, web typecheck and pnpm guard all clean. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
57d49ed798
|
fix(web): increase project meta line-height to prevent descender clipping (#834)
* fix(web): increase project meta line-height to prevent descender clipping The project type label (e.g. 'blog-post · Neutral Modern') had its descender characters (g, p, y) clipped by the header bottom border. Increasing line-height from 12px to 15px gives 11.5px font enough vertical space for lowercase descenders to render without being cut off. * fix web: bump project meta line-height and parent max-height to fit descenders .app-project-title .meta had line-height: 12px on font-size: 11.5px, leaving only 0.5px of vertical breathing room. Descender glyphs (g, p, q, y, j) extend below the baseline and were clipped by overflow: hidden. Bump .meta line-height from 12px to 15px and .app-project-title max-height from 31px to 32px so the full 16px + 1px gap + 15px = 32px budget fits without clipping. Refs: https://github.com/nexu-io/open-design/pull/834 |
||
|
|
56bf6ee1b6
|
feat: agent-callable research command and /search (#615)
* feat: pre-generation research (Tavily) for grounded generation
Adds an optional pre-generation research step so the agent can produce
slides / prototypes / decks grounded in real sources instead of guessing.
User flow:
1. Settings -> Tavily Search -> paste API key (or set TAVILY_API_KEY).
2. Click the new Research button in the chat composer.
3. On send, the daemon runs a Tavily search, prepends the findings
as a <research_context> block ahead of the system prompt, and
spawns the agent. Research progress shows up as status pills in
the chat stream; the agent cites sources inline as [1]/[2]/...
Phase 1 surface:
- Single provider (Tavily), single depth ('shallow'), no LLM
synthesis pass (Tavily's `answer` is the summary).
- Composer toggle only; no popover / depth picker yet.
- Reuses the existing `status` SSE agent payload + StatusPill UI
so no new event variants or renderer code are needed.
Layers touched:
- contracts: ResearchOptions / Source / Findings DTOs;
ChatRequest.research; export from index.
- daemon: apps/daemon/src/research/{index,tavily}.ts orchestrator
+ provider; tavily added to MEDIA_PROVIDERS and ENV_KEYS; hook
in startChatRun before prompt assembly.
- web: ChatComposer toggle + ChatSendMeta; threaded through
ChatPane / ProjectView / streamViaDaemon into ChatRequest.
Side fix (required to land the feature, but useful on its own):
contracts internal relative imports lacked the `.js` suffix that
NodeNext module resolution requires. This was already breaking
`pnpm --filter @open-design/daemon typecheck` on main; without the
fix, none of the new research types were visible to the daemon.
All internal contracts imports now carry `.js`.
Spec: specs/current/research-feature.md (phases 2-4 outlined for
follow-up: composer popover, multi-provider, deep recursion, example
skills with research_recommends).
Verified:
- pnpm --filter @open-design/contracts typecheck/test
- pnpm --filter @open-design/daemon typecheck (the chokidar
project-watchers test is a pre-existing flake, unrelated)
- pnpm --filter @open-design/web typecheck
- node scripts/verify-media-models.mjs
* fix(daemon): clamp Tavily max_results to 20
Tavily's /search endpoint requires `max_results` in [0, 20]; sending a
larger value (e.g. when `research.depth: "deep"` resolves to 30) returns
400 and `runResearch` silently falls back to no-research. Clamp at the
provider boundary so Phase 2 depth tiers above 20 still produce results
instead of failing the request.
Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)
* Remove stale research merge leftovers
* Add agent-callable research search
* Fix Indonesian locale typecheck
* Fix research command invocation edge cases
* Harden slash search prompt expansion
* Honor research source caps in command contract
* Require search reports in design files
* Add research data provider settings
* Wire web research provider fallback order
* Update research provider fallback wording
* Revert "Update research provider fallback wording"
This reverts commit
|
||
|
|
7107623ee2
|
test: expand entry and settings automation coverage (#811)
* test: harden new project panel metadata coverage * test: add settings and connector sync coverage * test: expand entry e2e coverage * test: satisfy exact optional property types in entry connector flow * test: keep entry Playwright coverage under e2e/ui * test: tighten coverage docs and settings test cleanup * test: drop e2e docs from the guarded package * docs: move automation coverage docs out of e2e * test: restore clipboard cleanup without delete * test: match composio save dialog behavior * test: avoid placeholder assertion after composio save * test: expect closeModal on settings saves * test: align settings save assertions with closeModal flags * test: fix settings save mocks * test: align composio replacement hint |
||
|
|
2bb029cb58
|
release: Open Design 0.5.0 (#820)
0.5.0 已从
|
||
|
|
31c3ceac53
|
fix: prevent comment popover header overflow when label is too long (#833)
- Add min-width: 0 and overflow: hidden to comment-popover-head div - Add text-overflow: ellipsis and white-space: nowrap to strong and span - Add flex: 0 0 auto to close button to keep it fixed width - Add title attribute to header div and close button for hover tooltip |
||
|
|
9c64ef1b2b
|
fix(web): wrap long note text inside picker/comment popover so it cannot push the layout sideways (#782) (#830)
The .board-note-item flex container holds a span (note text) and a Remove button. The span had no width hints, so an unbroken long string (URL, hash, base64, etc.) tried to fit on one line and pushed the row wider than the 320px popover, distorting the overlay's right edge and the surrounding picker UI. Added flex: 1 + min-width: 0 + overflow-wrap: anywhere to the note span. flex: 1 lets the span take remaining width next to the Remove button; min-width: 0 lifts the default flex-item min-content floor so shrinking actually works; overflow-wrap: anywhere allows the long string to break at any character when natural word boundaries aren't enough. No layout change for normal-length notes. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
c3c1b7c7b9
|
fix(web): wrap comment-popover action row so the Save/Sending button cannot exceed the popover edge (#779) (#829)
The .comment-popover-actions row laid out four buttons (Remove, Add note, Save comment, Send to chat / Sending...) with display: flex, justify-content: space-between, no flex-wrap, and no per-child shrink ceiling. The popover itself is width: min(320px, calc(100% - 28px)) with 10px padding, leaving roughly 300px of inner room. Real button labels (especially 'Save comment' + 'Send to chat' + 'Add note' together) exceed that, so the rightmost button visibly spilled past the popover's right edge. The 'Sending...' state in the screenshot is just where the user happened to notice it; the underlying overflow is independent of the button text. Added flex-wrap: wrap so the row breaks onto a second line when the labels do not fit, and a max-width: 100% on direct children so a single oversized button collapses to the row width instead of pushing the row out. No layout change at widths where the buttons already fit. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
5315a7dcae
|
add social-media-matrix-tracker template skill (#810)
* add social-media-matrix-tracker template skill
Package a new template-mode live-artifacts skill for a cinematic social media matrix dashboard, including a default example and reusable template seed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(skill): unblock CI for social-media-matrix-tracker-template
- Add skill id to DE/FR/RU EN-fallback skill lists in content.{ts,fr.ts,ru.ts}
to satisfy localized-content.test.ts coverage assertion (Validate workspace CI).
- Expand SKILL.md triggers with social analytics / content performance / TikTok /
Instagram / YouTube + 中文 关键词 (新媒体 / 数据看板 / 抖音 / 小红书) per
reviewer feedback (lefarcen P2#7).
Addresses merge-blocking CI failure flagged by @mrcfps and the discoverability
gap flagged by @lefarcen. Other code-level findings (chart precision, drag
overlay, tooltip clipping, touch input) will be addressed in follow-up commits.
* fix(i18n): add Indonesian translations for Cloudflare Pages deploy keys
Adds 18 fileViewer.* keys for Cloudflare Pages deploy support that were
introduced on main but never localized to id.ts, blocking typecheck for
any branch synced with main.
Translations cover: deploy provider selection (Vercel/Cloudflare Pages),
Cloudflare API token + Account ID inputs, generic provider deploy/error
messaging.
* fix(skill): address all P2 reviewer findings on social-media-matrix-tracker-template
Code fixes (template.html):
- ROI chart redraw now respects per-chart decimals from chartState (was hard-coded 0, lost roiChart precision after first interaction)
- Drag overlay state now cleared BEFORE redraw on mouseup, and on mouseleave during drag (was leaving stale overlay until next interaction)
- mixChart hover now updates insights focus panel with stack breakdown (was tooltip-only)
- sentimentChart hover now updates insights focus panel with sentiment label + share (was tooltip-only)
- Tooltip now measures itself + clamps inside viewport with edge-flip (was clipping at right/bottom edges)
- Added touchstart/move/end/cancel handlers + keyboard arrow-key navigation + Escape (was mouse-only, unusable on touch devices and keyboards)
- drawLineChart guards 0/1-element datasets (renders 'No data' or single labelled dot instead of NaN axis labels / Infinity min-max)
Docs (SKILL.md):
- Workflow now explicitly mentions tooltip clamping, panel update on every chart type, and touch/keyboard a11y
- Added 'Adapting the sample data safely' section with array shape, unit, decimals, and KPI lock-step contract
- Output contract now spells out artifact wrapper requirements, identifier convention, no external CDN/fonts, and single-document rule (no parallel index.html)
Addresses lefarcen P2 #1-5, P2 #7-10, and P3 #6.
* fix(skill): mrcfps round-2 review — fix touch dispatch + sync example.html
mrcfps Looper bot 0.6.2 caught two regressions in the round-1 fix push:
1. Touch dispatch threw TypeError: the previous adapter built a real
Event then Object.assign-ed clientX/clientY/target onto it. Event.target
is a read-only getter, so the assignment threw before the synthetic
mousedown ever reached the line chart. Touch support was effectively
broken on real devices.
Replaced with plain pointer objects passed directly into dedicated
handleTouchStart / handleTouchMove / handleTouchEnd handlers (which
reuse nearestByMouse, redraw, updateInsights, etc). No more synthetic
event dispatch, no read-only field assignment.
2. example.html was untouched in round 1, so the showcase that users
open directly still had x+14/y+14 tooltip clipping, no touch support,
no decimals state, no overlay-clearing fix, no insights update on
mix/sentiment hover, no short-series guard — all the things the
reusable template was just fixed for.
example.html is identical to template.html except for the JS block
(verified via diff of body + data calls), so we copy template.html
over example.html so the two stay in lock-step. Future template fixes
should mirror by 'cp assets/template.html example.html' until we
adopt build-time generation.
---------
Co-authored-by: tuolaji <tuola@tuolajideMacBook-Air.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Tuola-waj <tuola@nexu.io>
|
||
|
|
d4b547caa7
|
fix(web): keep saved Composio API key indicator visible while typing a replacement (#741) (#751)
The saved-key badge was wired to `isSavedState = apiKeyConfigured && !hasPendingEdit`, which made it disappear on the first keystroke as soon as the user started typing a draft replacement. Users reading the settings panel saw the saved key indicator vanish before they had clicked Save and reasonably assumed the stored credential had already been overwritten or removed. Credential editing is a high-trust workflow; a UI that fakes a state change before the durable write is the wrong default. Replaced the boolean derivation with a single helper `deriveComposioCredentialState` returning one of `empty | pending-new | saved | saved-pending`. The component now shows the saved-key badge for both `saved` and `saved-pending`, so the indicator stays anchored while the user types. The hint text differentiates all four states so the unsaved-replacement case is still clearly called out. Helper is exported and unit-tested in `apps/web/tests/components/SettingsDialog.test.ts` against the empty, pending-new, saved, and saved-pending states plus the whitespace-only-draft edge case that should still resolve to `saved`. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
1292fc5c41
|
fix(i18n): default id locale to English for untranslated keys (#822)
The Indonesian locale was added in |
||
|
|
988fd6db5e
|
feat: import existing local folder as project (#597) (#624)
* feat(contracts): types for folder-import endpoint
Add ImportFolderRequest, ImportFolderResponse to the public contract
surface. Extend ProjectMetadata with a baseDir field — when set, the
project's files live at this absolute path instead of .od/projects/<id>/.
Stored as the realpath() result so symlinks cannot redirect later writes.
Refs nexu-io/open-design#597
* feat(daemon): support metadata.baseDir for folder-rooted projects
Add resolveProjectDir() and metadata-aware variants of listFiles,
readProjectFile, writeProjectFile, ensureProject so a project's files
can live under metadata.baseDir (the user's chosen folder) instead of
.od/projects/<id>/. metadata.baseDir is opt-in — projects without it
keep the existing .od/projects/<id>/ behavior unchanged.
When listFiles walks a baseDir-rooted project, it skips conventional
build / install dirs (node_modules, .git, dist, build, .next, .nuxt,
.turbo, .cache, .output, out, coverage, __pycache__, .venv, vendor,
target, .od, .tmp) so the file panel stays focused on design content
instead of being dominated by lockfiles and node_modules.
Add detectEntryFile() — best-effort lookup for index.html or any
.html at the folder root, used by the import endpoint to seed the
initial active tab.
Refs nexu-io/open-design#597
* feat(daemon): add POST /api/import/folder endpoint
Creates a project rooted at the submitted local folder. metadata.baseDir
points at that folder and OD reads / writes there directly — no copy,
no shadow tree, mirroring how Cursor / Claude Code / Aider behave. The
user owns the workspace and is responsible for their own version
control.
Safety:
- baseDir is canonicalized via fs.promises.realpath() at import time so
user-controlled symlinks can't redirect later writes. resolveSafe
enforces the bounds check against the literal stored path; without
realpath, a symlink (e.g. ~/sneaky → /etc) would let writeProjectFile
escape the project tree at every later call because the OS follows
the symlink at open() time.
- Post-realpath lstat ensures the canonical target is itself a real
directory (defense-in-depth).
- The data directory (RUNTIME_DATA_DIR) and its descendants are
refused after symlink resolution so a redirect into the daemon's
own state can't masquerade as a project import.
The web client wires this through state/projects.ts → App.tsx,
landing the user on the auto-detected entry file when present.
Refs nexu-io/open-design#597
* feat(desktop): expose native folder picker to renderer
Adds an Electron preload script that exposes window.electronAPI.pickFolder
via contextBridge. Wires dialog.showOpenDialog through ipcMain so the
web UI can open a native folder selector for project import. Browser-only
users fall back to a text input for the absolute path (handled in the
web layer); the picker stays an optional convenience on the desktop
binary.
ipcMain.handle() registers handlers in an internal map that is not
exposed via eventNames(), so the natural-looking guard
if (!ipcMain.eventNames().includes('dialog:pick-folder')) ipcMain.handle(...)
is always true. On a second createDesktopRuntime() call (dev hot-reload,
packaged-vs-electron mode swap) the body re-runs and ipcMain.handle()
throws 'Attempted to register a second handler'. Use removeHandler()
+ handle() unconditionally — removeHandler() is a documented no-op
when nothing is registered, making the pair idempotent.
Includes *.cts in the apps/desktop tsconfig so the preload script is
typechecked.
Refs nexu-io/open-design#597
* feat(web): add 'From existing folder' option to New Project
UI surface for the import flow:
- A new 'Open folder' affordance in NewProjectPanel that uses the
native picker on Electron (window.electronAPI.pickFolder) and falls
back to an absolute-path text input in the browser.
- importFolderProject() in state/projects.ts: typed wrapper around
POST /api/import/folder using @open-design/contracts types.
- App.tsx wires the response: prepend the new project to the list,
navigate to it, and select the auto-detected entry file as the
active tab.
Skill / design-system pickers from the existing prototype tab are
reused — folder import is a project-creation flow, not a separate
project type.
Refs nexu-io/open-design#597
* docs(architecture): document folder-import endpoint
Adds POST /api/import/folder to the daemon API table and a 'Folder
import' section explaining the single-mode design (direct read/write
in metadata.baseDir, mirroring Cursor / Claude Code / Aider), the
realpath() canonicalization, the RUNTIME_DATA_DIR refusal, and the
SKIP_DIRS list applied to listFiles for baseDir-rooted projects.
Refs nexu-io/open-design#597
* test(daemon): unit + integration tests for folder import
Two new files:
apps/daemon/tests/folder-import-projects.test.ts (13 unit tests):
- resolveProjectDir behavior under all metadata combinations,
including the fallback when baseDir is relative and the
isSafeId-bypass when baseDir is set
- detectEntryFile: index.html priority, .html fallback, null when
no html, no descent into subdirs
- listFiles with metadata.baseDir: walk, SKIP_DIRS hides node_modules
/ .git / dist, back-compat for projects without baseDir
apps/daemon/tests/folder-import-route.test.ts (10 integration tests):
- Happy path: baseDir stored in metadata, importedFrom='folder',
conversation created, entry file detected
- Error paths: missing baseDir, empty, relative, non-existent,
pointing at a file
- Security: realpath canonicalization (the symlink test was the one
that surfaced the original /var vs /private/var mismatch in
RUNTIME_DATA_DIR comparison on macOS)
- Security: a symlink that resolves into RUNTIME_DATA_DIR is rejected
after realpath, not before
Refs nexu-io/open-design#597
* fix(daemon): wire baseDir metadata into chat + deploy reads
Two bugs caught in Codex automated review of #624:
1. chat-route was passing the metadata object directly as the listFiles
opts argument: `listFiles(PROJECTS_DIR, projectId, chatMeta)`. The
listFiles contract reads opts.metadata, not opts itself, so this
silently fell back to .od/projects/<id>/ instead of the imported
folder. existingProjectFiles was empty for baseDir-rooted projects.
Wrap as `{ metadata: chatMeta }`.
2. deploy.ts read project files via readProjectFile without the
metadata third argument, so for baseDir-rooted projects the deploy
and preflight endpoints would look in .od/projects/<id>/ and fail
with file-not-found instead of reading the imported folder. Thread
options.metadata through buildDeployFilePlan → readProjectFile and
pass project?.metadata at the two server.ts callsites
(`POST /api/projects/:id/deploy` and the preflight endpoint).
Add a regression test that locks the listFiles contract: passing a
bare metadata object as opts must NOT scan baseDir — it must fall back
to the standard project dir, otherwise callers can leak the wrong
folder by mistake.
Refs nexu-io/open-design#597, #624 (Codex review)
* fix(daemon): ensure correct metadata handling in folder import
Addressed issues with metadata handling in folder import functionality. Updated the listFiles and readProjectFile methods to correctly utilize the metadata.baseDir, ensuring that project files are read from the intended directory. Added regression tests to verify that passing a bare metadata object does not inadvertently scan the baseDir, maintaining the integrity of project file access.
Refs nexu-io/open-design#597
* fix(daemon): security hardening from Codex review of #624
P1 findings from automated review:
1. POST /api/projects + PATCH /api/projects/:id rejected
client-supplied metadata.baseDir. baseDir is privileged: it lets a
project root inside the user's filesystem, and the realpath() +
RUNTIME_DATA_DIR reentry checks live only on /api/import/folder.
Allowing it on the generic create/patch path lets an attacker
smuggle e.g. /etc through and bypass every import-time guard.
Both endpoints now refuse a baseDir field with 400.
2. resolveSafeReal() helper: realpath()s each candidate path (or its
longest existing prefix for write paths) and re-validates against
realpath(projectRoot). The original resolveSafe() only did a
string-prefix check, which was fooled by symlinks *inside* a
baseDir-rooted project. A repo containing 'assets -> /Users/me/.ssh'
passed the literal prefix check but readFile() followed the link
at open() time. resolveSafeReal() is now used by readProjectFile,
writeProjectFile, and deleteProjectFile.
3. Multer chat-upload destination now resolves to metadata.baseDir for
imported folder projects via a module-level lookup wired to db at
startServer() boot. Previously attachments landed in
.od/projects/<id>/ even for baseDir projects, so the agent (which
runs with cwd=baseDir) couldn't open them.
P2 findings:
4. searchProjectFiles threads metadata through listFiles +
resolveProjectDir so /api/projects/:id/search hits the right tree.
5. buildProjectArchive + buildBatchArchive now accept metadata so
'Download .zip' works for imported folder projects.
6. Watcher subscribe() resolves to baseDir for imported projects so
live-reload SSE actually fires when the user edits files in their
own folder. Registry stays keyed by the canonical directory.
7. Template snapshotting reads source-project files with metadata
so a template can be saved from a baseDir-rooted source.
Tests:
- Regression: POST /api/projects with metadata.baseDir → 400.
- Regression: descendant symlink (assets/leak.txt -> /etc/hosts) is
refused on the raw read endpoint.
Refs nexu-io/open-design#597, #624 (Codex P1+P2 review)
* fix(daemon): close two regressions found in #624 review round 2
@mrcfps caught two more correctness gaps:
1. Archive root symlink escape — buildProjectArchive accepts an optional
?root=<subdir> param to scope the zip to a subdirectory. The path was
resolved with the string-only resolveSafe(), so a directory symlink
inside an imported folder (docs -> /Users/me/.ssh) passed the prefix
check and collectArchiveEntries() then walked outside the project
tree. Switch to the symlink-aware resolveSafeReal() — the same one
that already protects raw read/write/delete paths. The walker itself
already skips dirent symlinks via !isDirectory && !isFile, so
canonicalizing the root is the only missing piece.
2. PATCH metadata wiped baseDir — updateProject() replaces metadata
wholesale. The previous guard only blocked an explicit baseDir
change, but a normal patch that *omits* baseDir (a UI editing
linkedDirs only sends { metadata: { kind, linkedDirs } }) silently
detached imported projects from their folder root. Subsequent
reads/writes/watch/deploy fell back to .od/projects/<id>.
Re-stamp the immutable folder-import fields (baseDir, importedFrom='folder')
from the existing project record onto the incoming patch when the
project is imported. A patch that supplies a *different* baseDir
still gets rejected as before; a patch that supplies the *same*
baseDir is accepted as a no-op. A patch on a non-imported project
that tries to set baseDir is also still rejected (preserves the
POST /api/projects guard from the previous round).
Tests:
- archive endpoint: ?root=<symlink-to-/etc> → 400.
- patch endpoint: PATCH that omits baseDir on an imported project keeps
baseDir intact (project still resolves to the user's folder after).
Refs nexu-io/open-design#597, #624 (Codex P1 round 2)
* fix(web): add Indonesian deploy provider copy
---------
Co-authored-by: INFINITY <valentyn.sotov@trendarena.app>
Co-authored-by: Siri-Ray <2667192167@qq.com>
|
||
|
|
bef8203ad9
|
fix: expand Codex picker coverage (#757)
* fix: add newer Codex model choices * fix: expand Codex picker coverage --------- Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com> |
||
|
|
09eb88f683
|
Add Cloudflare Pages artifact deployment
Adds Cloudflare Pages artifact deployment support. |
||
|
|
8630fd380a
|
feat(daemon): close pi adapter parity gaps
Closes pi adapter parity gaps for image paths, extra allowed dirs, error events, and sendAgentEvent routing. |
||
|
|
168cb8ab4d
|
feat(web): add batch delete for selected design files (#771)
Adds batch deletion for selected design files. |
||
|
|
7ce4eb4e82
|
feat(prompt-templates): add Notion-style team dashboard (Live Artifact) (#799)
* feat(prompt-templates): add Notion-style team dashboard (Live Artifact)
Adds a single image prompt template under the Live Artifact category — a
Notion-native team dashboard mockup with KPI grid, 7-day sparkline,
activity feed, and linked-database task table.
This is the first prompt template to use the curated Live Artifact
category, whose de/fr/ru localization slots were already reserved in
apps/web/src/i18n/content{,.fr,.ru}.ts. Only the new tag 'live-artifact'
is added to each locale's PROMPT_TEMPLATE_TAGS map (+1 line each) so the
arrayContaining check in e2e/tests/localized-content.test.ts continues
to pass.
Template-level only: no new surface, no loader changes, no schema or
TypeScript type changes.
* fix(prompt-templates,i18n): register 'Live Artifact' category and template ID fallback for de/fr/ru
CI's e2e/localized-content.test.ts enumerates LOCALIZED_CONTENT_IDS from
apps/web/src/i18n/content.ts and asserts:
- ids.promptTemplates === sorted(all template ids in prompt-templates/)
- ids.promptTemplateCategories ⊇ all categories actually used by templates
- ids.promptTemplateTags ⊇ all tags actually used by templates
The new notion-team-dashboard-live-artifact template introduced both
the first 'Live Artifact' category and the first prompt-template id
without a copy translation, so each locale needs:
- 'Live Artifact' added to *_PROMPT_TEMPLATE_CATEGORIES (currently
consumed via arrayContaining; order doesn't matter)
- 'notion-team-dashboard-live-artifact' listed in
*_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK so it joins ids.prommplates
via the EN-fallback path (no per-locale title/summary copy needed)
The 'live-artifact' tag was already added to *_PROMPT_TEMPLATE_TAGS in
the previous commit on this branch.
3 files changed, +6 / -3.
* fix(i18n): register Live Artifact category + template id fallback (CI repair on #799)
* fix(i18n): register Live Artifact category + template id fallback (CI repair on #799)
* fix(i18n): register Live Artifact category + template id fallback (CI repair on #799)
* fix(prompt-templates): scrub live/connector affordances from notion-team-dashboard prompt (#799 review)
Reviewers (mrcfps, lefarcen) flagged that even with the amber "Sample
data — design preview" banner and the "(sample data)" footer, the inner
prompt blob still asked the model to render UI affordances claiming a
real Notion / Composio connector binding ("Live · synced" pill, "Last
refreshed just now", "Refresh from Notion" blue button, callout saying
numbers are "pulled from your {workspace} Notion workspace via the
Composio connector"). That contradicts the prompt-only contract and
reintroduces the #778 mock-honesty concern.
Rewrite the prompt blob so every UI element is consistently presented
as seeded sample data:
- topbar.preview_pill (was live_pill):ample · design preview' pill
with explicit negative instruction no to render any live/sync pill
- page_header.meta_row: drop 'Last refreshed', 'Auto' toggle, and the
'Refresh from Notion' blue button; explicit DO NOT instructions
- callout: 'prompt-only design preview ... seeded sample data ... not
pulled from a real Notion workspace and not refreshed via the
Composio connector. For real refreshable / connector-backed Live
Artifacts, use the live-artifact skill.' Also removes the bare
'{workspace}' placeholder that was not using {argument ...} syntax
(P2 nit from lefarcen).
- activity_feed_card.subtitle: 'Notion-style seeded activity for
design preview' (was 'From Notion')
- linked_database.title/subtitle: marked '(sample)' / 'seeded sample
rows · no live connector binding'
- linked_database.row_styles: explicit negative instruction not to
render an 'Updated ↻' refresh badge
- footer: 'Notion-style sample data · seeded design preview, not bound
to any Notion workspace or Composio connector'
- honesty_rule: enumerates all live/sync/refresh affordanche
generator must NOT render
Top-level metadata (id, title, summary, category, tags, model, aspect,
previewImageUrl, source) is unchanged. Preview PNG already shows the
amber banner and a layout without a Refresh button, so it matches the
new in-prompt language.
* fix(prompt-templates): scrub live/connector affordances from notion-team-dashboard prompt (#799 review)
Reviewers (mrcfps, lefarcen) flagged that even with the amber "Sample data — design preview" banner and the "(sample data)" footer, the inner prompt blob still asked the model to render UI affordances claiming a real Notion / Composio connector binding ("Live · synced" pill, "Last refreshed just now", "Refresh from Notion" blue button, callout saying numbers are "pulled from your {workspace} Notion workspace via the Composio connector"). That contradicts the prompt-only contract and reintroduces the #778 mock-honesty concern.
Rewrite the prompt blob so every UI element is consistently presented as seeded sample data:
- topbar.preview_pill (was live_pill): 'Sample · design preview' pill with explicit negative instruction not to render any live/sync pill.
- page_header.meta_row: drop 'Last refreshed', 'Auto' toggle, and the 'Refresh from Notion' blue button; explicit DO NOT instructions for all three.
- callout: 'prompt-only design preview ... seeded sample data ... not pulled from a real Notion workspace and not refreshed via the Composio connector. For real refreshable / connector-backed Live Artifacts, use the live-artifact skill.' Also removes the bare '{workspace}' placeholder that was not using {argument ...} syntax (P2 nit from lefarcen).
- activity_feed_card.subtitle: 'Notion-style seeded activity for design preview' (was 'From Notion').
- linked_database.title/subtitle: marked '(sample)' / 'seeded sample rows · no live connector binding'.
- linked_database.row_styles: explicit negative instruction not to render an 'Updated ↻' refresh badge.
- footer: 'Notion-style sample data · seeded design preview, not bound to any Notion workspace or Composio connector'.
- honesty_rule: enumerates all live/sync/refresh affordances the generator must NOT render.
Top-level metadata (id, title, summary, category, tags, model, aspect, previewImageUrl, source) is unchanged. Preview PNG already shows the amber banner and a layout without a Refresh button, so it matches the new in-prompt language.
* fix(prompt-templates): scrub live/connector affordances from notion-team-dashboard prompt (#799 review)
Reviewers (mrcfps, lefarcen) flagged that even with the amber "Sample data — design preview" banner and the "(sample data)" footer, the inner prompt blob still asked the model to render UI affordances claiming a real Notion / Composio connector binding ("Live · synced" pill, "Last refreshed just now", "Refresh from Notion" blue button, callout saying numbers are "pulled from your {workspace} Notion workspace via the Composio connector"). That contradicts the prompt-only contract and reintroduces the #778 mock-honesty concern.
Rewrite the prompt blob so every UI element is consistently presented as seeded sample data:
- topbar.preview_pill (was live_pill): 'Sample · design preview' pill with explicit negative instruction not to render any live/sync pill.
- page_header.meta_row: drop 'Last refreshed', 'Auto' toggle, and the 'Refresh from Notion' blue button; explicit DO NOT instructions for all three.
- callout: 'prompt-only design preview ... seeded sample data ... not pulled from a real Notion workspace and not refreshed via the Composio connector. For real refreshable / connector-backed Live Artifacts, use the live-artifact skill.' Also removes the bare '{workspace}' placeholder that was not using {argument ...} syntax (P2 nit from lefarcen).
- activity_feed_card.subtitle: 'Notion-style seeded activity for design preview' (was 'From Notion').
- linked_database.title/subtitle: marked '(sample)' / 'seeded sample rows · no live connector binding'.
- linked_database.row_styles: explicit negative instruction not to render an 'Updated ↻' refresh badge.
- footer: 'Notion-style sample data · seeded design preview, not bound to any Notion workspace or Composio connector'.
- honesty_rule: enumerates all live/sync/refresh affordances the generator must NOT render.
Top-level metadata (id, title, summary, category, tags, model, aspect, previewImageUrl, source) is unchanged. Preview PNG already shows the amber banner and a layout without a Refresh button, so it matches the new in-prompt language.
---------
Co-authored-by: joeylee12629-star <joeylee12629-star@users.noreply.github.com>
|
||
|
|
cb92c93ae0
|
Migrate beta release publishing to R2 (#805)
* Prebundle standalone web packaged runtime * Harden mac standalone prebundle policy * Prebundle mac daemon packaged runtime * Prune mac Electron locales * Maximize mac release artifact compression * Publish beta mac artifacts to R2 * Use remote R2 uploads for beta releases * Fail fast on beta R2 access issues * Use S3-compatible uploads for beta R2 releases * Decouple beta versioning from GitHub releases * Remove legacy beta metadata source * Address release beta review notes |
||
|
|
5abca505b1
|
add FlowAI live dashboard template skill (#801)
* add flowai live dashboard template skill Introduce a new template-mode skill under the live-artifacts scenario with a default interactive example and seed template so users can generate polished, refresh-ready team dashboards quickly. Co-authored-by: Cursor <cursoragent@cursor.com> * add preview screenshot for flowai live dashboard template Attach the provided dashboard screenshot under docs/screenshots/skills so the template contribution includes a visual preview artifact. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(flowai-template): reposition as static prototype dashboard skill Address review feedback on PR #801: - SKILL.md: drop `scenario: live-artifacts` and live-related triggers; align with peer single-page dashboard skills using `mode: prototype` + `scenario: operations` so the four-file live-artifact contract no longer applies. - references/checklist.md: rewrite quality gates around the static prototype scope (export-from-DOM, responsive breakpoints, theme-aware charts). - assets/template.html: - CSV export now reads every visible row from the table DOM, including the Workflow column, instead of a hardcoded fixture. - Add 1300px and 720px breakpoints; the main grid stacks to one column, stat cards fall back to two then one, tabs wrap, table scrolls horizontally on phones. - Move chart colors into CSS variables (--chart-stroke, --chart-fill, --chart-axis, --chart-bar-label, --chart-bar-value) so dark-mode toggling re-derives them; chart canvases are re-rendered after theme switch. - Hash-sync tabs (#members | #details | #activity), animate the role bar chart only on first reveal of the details tab, fall back when CanvasRenderingContext2D.roundRect is unavailable, add Esc to exit zoom and prevent tooltip clipping. - example.html: title cleanup to match new skill identity. Localized content: - Add `flowai-live-dashboard-template` to DE/FR/RU SKILL_IDS_WITH_EN_FALLBACK lists in apps/web/src/i18n so the e2e localized-content test passes. --------- Co-authored-by: tuolaji <tuola@tuolajideMacBook-Air.local> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Tuola Ge <gexingli@refly.ai> |
||
|
|
555dbebfe2
|
fix(web): add alert when pdf export popup is blocked (#664)
Fixes PDF export feedback when popup blockers prevent opening the export preview. |
||
|
|
55aa24167b
|
add live-dashboard skill (#778)
* add live-dashboard skill Notion-style team dashboard rendered as a Live Artifact. Wires the OD 0.4.0 connector catalog (#381) end-to-end: refresh-on-open, manual Refresh tween, auto-refresh, stale state. Falls back to seeded mock data when no connector is bound. * address PR #778 review comments P1 — security and correctness: - skills/live-dashboard/assets/template.html · skills/live-dashboard/example.html: escape every connector-derived string before innerHTML interpolation. Adds a tiny e() helper and routes feed.who/action/target/suffix/icon, row.title/icon/due/prio, person.name/color/id, KPI label/delta through it. Closes lefarcen #3200122795 + #3200122820. - skills/live-dashboard/SKILL.md (live behavior section): align connector poll URL with references/connectors.md — POST /api/od/connectors/poll with { project, read } body, not /api/od/connectors/<id>/poll. Closes codex bot #3200100897. - apps/web/src/i18n/content{,.ru,.fr}.ts: register live-dashboard in DE_/RU_/FR_SKILL_IDS_WITH_EN_FALLBACK so the localized-content e2e check passes. Closes mrcfps #3200122059. - skills/live-dashboard/references/connectors.md: prepend a Status callout that names skills/live-artifact/ as the canonical file/CLI live-artifact contract and frames the HTTP shape as a forward-looking proposal sitting alongside it (out-of-the-box the artifact runs on seeded data; only seedNextChange() needs swapping when POST /api/od/connectors/poll lands). Closes lefarcen #3200122811. P2 — quality and honesty: - skills/live-dashboard/references/connectors.md: rewrite the auth_ref resolution step to match apps/daemon/src/media-config.ts (OD_MEDIA_CONFIG_DIR → OD_DATA_DIR → <projectRoot>/.od/media-config.json, $HOME/~/relative paths handled via expandHomePrefix). Closes codex bot #3200100906. - skills/live-dashboard/example.html: switch the live-pill to a sticky Sample data state with a grey static dot, rewrite the callout to admit the figures are seeded fixtures, retitle the toast and the refresh tooltip, and refuse to flip to Live · synced inside updateTimes(). Adds a .pill-live.sample CSS variant. Closes lefarcen #3200122823. - skills/live-dashboard/assets/template.html: hoist <meta name=od:project> from <body> into <head>. Closes lefarcen #3200122832. - skills/live-dashboard/assets/template.html · example.html: add role=button + tabindex=0 + aria-current to every clickable .ws / .side-search / .nav-item, and wire a single document-level keydown handler that maps Enter/Space to a synthetic click for any role=button div (skipping real buttons / anchors / form controls). Closes lefarcen #3200122837. - skills/live-dashboard/assets/template.html: implement the KPI tween + flash + snapshotKpi() the SKILL.md prose already promised — first render builds escaped cards, subsequent renderKpi(prev) calls tween numeric values and flash() the cells that actually changed; refresh() now calls snapshotKpi() before mutating state and forwards prev. SKILL.md spells out the wire-up. Closes lefarcen #3200122839. * gate KPI tween + flash + row/feed highlight on prefers-reduced-motion Addresses mrcfps's non-blocking review item on PR #778 (comment #3200614137, template.html:453). The CSS @media (prefers-reduced-motion: reduce) block already neutralizes CSS animations and transitions, but the new JS-driven helpers kept moving for opted-out users: - tweenText() scheduled requestAnimationFrame updates for 600ms - flash() toggled the .flash highlight class for 700ms - renderFeed()/renderRows() applied .feed-row.new / .db-row.changed classes which carry transient backgrounds even when their CSS animations are off Both runtimes (assets/template.html and example.html) now share a reduceMotion() helper (window.matchMedia('(prefers-reduced-motion: reduce)').matches). When it returns true: - tweenText()/tween() set the final value immediately and return - flash() returns without touching the class - renderFeed()/renderRows() pass null as the highlight id so the .new / .changed classes are never applied Normal-motion users see the existing tween + flash + highlight pulse unchanged. Keeps the P0 prefers-reduced-motion row in references/checklist.md honest for agents that copy this template verbatim. --------- Co-authored-by: joey <joey@joeydeMacBook-Air.local> Co-authored-by: joeylee12629-star <joeylee12629-star@users.noreply.github.com> |
||
|
|
24b234e3c7
|
fix chat pane overflow (#740)
Co-authored-by: KevinWu-Pm <wx19950829@163.com> Co-authored-by: DoTheWorkNow <260719655+DoTheWorkNow@users.noreply.github.com> |
||
|
|
84ac93c945
|
fix(daemon): extend OpenAI image request timeouts (#788) | ||
|
|
324b20ad81
|
fix(css): reduce ws-tabs-bar scrollbar width to avoid filename overlap (#781)
Co-authored-by: Siri-Ray <2667192167@qq.com> |
||
|
|
b95ba5e79e
|
add waitlist-page skill (#555)
* add waitlist-page skill
* fix(waitlist-page): address PR review feedback
- Remove novalidate from example.html form
- Ensure checkValidity() guard present in both template and example
- Remove required from firstname input in template
- Add token escaping rules to SKILL.md workflow (step 9)
- Add token mapping/fallback rules for BORDER/SUCCESS/STRIPE/DECO (step 7)
- Fix mobile quality gate to be measurable (375x667, 390x844)
- Promote hardcoded #fff, rgba(0,0,0,0.9), rgba(255,255,255,0.9) to
CSS variables (--btn-label, --ticker-bg, --ticker-fg) in template
- Create references/checklist.md with P0/P1/P2 tiers; countdown timer
is now a hard P0 prohibition; a11y gate split into six specific checks"
* fix: resolve P0 color and accessibility issues
- Add role=status to success messages for screen reader announcement
- Replace all hardcoded hex/rgba colors with template tokens
- Update SKILL.md with comprehensive color token mapping rules
- SVG decorations now use CSS variables instead of hardcoded strokes
* fix: address PR review feedback on scope, scrolling, and font tokens
Fixes:
- Restore pricing-page files accidentally deleted in previous commit
(skills/pricing-page/SKILL.md and example.html now back on branch)
- Remove temp-original.html scratch file from commit
- Fix mobile viewport scrolling: change 'height: 100vh; overflow: hidden'
to 'min-height: 100svh; overflow-x: hidden; overflow-y: auto'
so content doesn't clip on 375×667 and 390×844 screens
- Split font tokens into URL-safe and CSS-safe variants:
* {{DISPLAY_FONT_URL}} and {{DISPLAY_FONT_CSS}} for display fonts
* {{BODY_FONT_URL}} and {{BODY_FONT_CSS}} for body fonts
This fixes encoding: spaces as '+' in Google Fonts URL, literal in CSS
- Update SKILL.md frontmatter with new font input fields
- Update token escaping rules to document the split
* fix: resolve token contract mismatch and remove hardcoded colors from example.html
P0 Fixes:
- Remove all hardcoded colors from example.html (except #2D6A4F for --success)
- Use CSS variables for all color values: --btn-label, --ticker-bg, --ticker-fg, --deco-stroke
- Fix gradient to use var(--deco) instead of hardcoded #D1632B
- Apply consistent color expressions across decorations and text
Token Contract Fixes:
- template.html now uses full CSS expressions for opacity-based colors:
* {{BORDER_EXPRESSION}} instead of {{BORDER_HEX}} (no # prefix)
* {{BTN_LABEL_EXPRESSION}} instead of {{BTN_LABEL_HEX}}
* {{TICKER_BG_EXPRESSION}}, {{TICKER_FG_EXPRESSION}}, {{DECO_STROKE_EXPRESSION}}
- Remove extra quotes from font tokens in template:
* --font-body: {{BODY_FONT_CSS}} instead of '{{BODY_FONT_CSS}}'
* Font tokens are already quoted if needed, no wrapping
- Update SKILL.md frontmatter with all color expression inputs and descriptions
- Update token mapping rules to clarify the new contract:
* Hex tokens: simple six-digit colors
* Expression tokens: full CSS values (rgba/color-mix), no # prefix
* Font tokens: CSS font-family values, no extra wrapping
- Update token escaping rules to reflect new contract
This ensures agents can follow SKILL.md instructions without producing invalid CSS.
* fix: remove final hardcoded colors from example.html - P0 complete
- Button text: #fff → var(--btn-label)
- Ticker background: rgba(0,0,0,0.9) → var(--ticker-bg)
- Ticker text: rgba(255,255,255,0.9) → var(--ticker-fg)
- Logo text: fill=white → fill=var(--btn-label)
All colors now derive from design system tokens. Only #2D6A4F (--success) allowed hardcoded exception.
* fix: correct --btn-label contrast for CTA readability
Change --btn-label from #1A1410 (same as button background) to #FDE8DF
(light background color) so button text has proper contrast against
the dark --accent button background.
This resolves the black-text-on-black issue that broke the main
email capture action and satisfies the checklist button contrast gate.
* fix: add visible focus indicator for input accessibility
P1 Accessibility Polish:
- Update .form-row input:focus to include outline and outline-offset
- Before: border-color only, removing default outline (no visible focus)
- After: border-color + 2px outline + 2px offset (clear focus indicator)
This satisfies the checklist P1 focus-style gate and ensures keyboard
users can see which form field has focus. Both example.html and
template.html updated so agents copy complete focus patterns.
* fix: remove hardcoded logo shadow color - P0 compliance
- Add --logo-shadow CSS variable derived from foreground
- example.html: box-shadow 0 2px 8px rgba(0,0,0,0.08) → var(--logo-shadow)
- template.html: add {{LOGO_SHADOW_EXPRESSION}} placeholder
- Update SKILL.md with logo_shadow_expression input and mapping rules
All colors in example.html now derive from design system tokens.
Ensures agents copy compliant reference without hardcoded shadow colors.
* fix: register waitlist-page skill in i18n localized content registry
Add waitlist-page to locale-specific skill fallback lists so the web
content coverage test passes when the new skill is discovered:
- apps/web/src/i18n/content.ts: Add to DE_SKILL_IDS_WITH_EN_FALLBACK
- apps/web/src/i18n/content.fr.ts: Add to FR_SKILL_IDS_WITH_EN_FALLBACK
- apps/web/src/i18n/content.ru.ts: Add to RU_SKILL_IDS_WITH_EN_FALLBACK
The skill falls back to English localization for now; localized
descriptions can be added to each locale file later.
Fixes: web content coverage test now passes (6/6 tests).
* fix: wire template and checklist into skill workflow as mandatory gates
Restructure waitlist-page SKILL.md workflow to enforce the hardened
template-based execution path:
- Add Preflight section: agents MUST read assets/template.html first
- Add explicit token mapping and escaping rules (steps 2-4)
- Add mandatory Validation section: run references/checklist.md P0/P1
gates BEFORE emitting artifact; fail fast if any P0 gate fails
- Update Quality gates section to emphasize template-based execution
and reinforce P0/P1 gate hierarchy
- Update Output section: only emit after P0 passes; re-validate on
iterations
This prevents agents from writing HTML from scratch or skipping the
hardened seed (template) and validation (checklist) that this PR adds.
* refactor(waitlist-page): replace literal logo placeholder with token
- Replaced `[LOGO]` with `{{LOGO_MARK}}` in template.html
- Added `logo_mark` to inputs in SKILL.md
- Updated mapping rules in SKILL.md to handle raw SVG or text for logo
- Updated P0 validation gates in SKILL.md and checklist.md to ensure logo replacement
* fix(waitlist-page): enforce strict escaping and sanitization for logo token
- Mandate HTML-escaping for text initials.
- Enforce strict allowlist-based sanitization for inline SVG (stripping `<script>`, `on*`, `<foreignObject>`, `href`, `xlink:href`, `url()`).
- Add fallback to escaped text initials for invalid/unsafe SVG.
* docs(waitlist-page): sync logo_mark frontmatter description with rules
- Updated the `logo_mark` input description in the SKILL.md frontmatter to explicitly outline the new requirements for HTML-escaped text or strict allowlist-sanitized SVG.
* fix(waitlist-page): add logo_fg_expression to guarantee contrast in logo mark text fallback
- Added `--logo-fg` CSS variable mapped to `{{LOGO_FG_EXPRESSION}}`.
- Updated `.logo-container` in `template.html` to inherit typography styles and apply `--logo-fg` for safe fallback when rendering escaped initials.
- Enforced WCAG AA contrast for logo initials against container background in `checklist.md`.
* refactor(waitlist-page): migrate hex color tokens to full css expressions
* refactor(waitlist-page): strict validation for color expression tokens to prevent CSS injection
* docs(waitlist-page): update validation summary to reflect strict color grammar
---------
Co-authored-by: Siri-Ray <2667192167@qq.com>
|
||
|
|
a8418ac730
|
Fix Windows link code folder dialog (#698)
* Fix Windows link code folder dialog * Add Windows folder dialog coverage * Complete Indonesian locale copy |
||
|
|
fcc37c6c2d
|
feat(i18n): add Indonesian locale | ||
|
|
6efac8887e
|
Improve Windows beta packaging and installer flow (#768)
* Optimize Windows packaged web output * Fix packaged contracts runtime build * Optimize Windows packaged size pruning * Prune Windows root Next payload * Remove Windows bundled Node runtime * Prune Windows standalone duplicate Next * Add tools-pack cache foundation * Cache Windows packaged build layers * Cache Windows workspace builds * Cache Electron-ready Windows app * Split Windows tools-pack module * Cache Windows dir build outputs * Split Windows pack build modules * Document Windows NSIS smoke namespace limits * Move Windows NSIS smoke note to agents guide * Optimize Windows beta packaging * Bump packaged beta base version * Improve Windows installer namespace UX * Improve Windows tools-pack cache keys * Stabilize Windows beta cache version keys * Cache Windows workspace build outputs * Optimize windows release beta cache layers * Cache windows release dependencies * Trim windows release cache before save * Refresh windows tools-pack cache key * Improve windows installer preflight prompts * Fallback NSIS installer strings to English * Fix Windows installer cleanup and preflight * Improve Windows NSIS state logging * Fix system NSIS Persian language alias * Use long-path removal for Windows uninstall * Fix mac tools-pack tests on Windows * Address Windows packaging review feedback * Fix Windows installer cache namespace isolation * Include web output mode in Windows tarball cache key * Use unique Windows release cache save keys |
||
|
|
38eb78a382
|
feat(web): add Inspect mode for live per-element style tuning | ||
|
|
bb2015766a
|
feat: Critique Theater Phase 5 (panel prompt template + system composer wiring) | ||
|
|
c00f89dbe4
|
fix(daemon): allow portless Origin in CORS whitelist for Chrome compatibility | ||
|
|
984797b3cd
|
fix: add DeepSeek v4 models to catalog (#722) | ||
|
|
25a3ffd298
|
fix(daemon): add legacy data dir migrator
Add a one-shot OD_LEGACY_DATA_DIR migrator so packaged Desktop users can recover 0.3.x repo .od data into the 0.4.x data root. The migrator stages payloads before promotion, refuses unsafe merges and symlinks, rolls back failed promotion or marker writes, and extends packaged daemon startup handling for long migrations while failing fast on daemon exits. Closes #710 |
||
|
|
9b501f12a5
|
Support overriding the Codex executable path (#755)
* Support overriding the Codex executable path * Replace save-as-template prompts with an in-app dialog * Seed local packaged app config from workspace * Fix packaged config and connection test overrides * Keep tools-pack mac config seeding self-contained * Require absolute CODEX_BIN overrides |
||
|
|
4fe3a37209
|
fix(web): widen settings subtitle max width
Allow the English settings subtitle to fit on one line in the settings modal while preserving wrapping on narrow viewports. |
||
|
|
01c22a87bd
|
fix(web): replace SketchEditor text prompt with modal
Replace the SketchEditor text tool's window.prompt flow with an in-app modal so it works in Electron desktop. |
||
|
|
0b2b456694
|
fix(daemon): deliver Copilot prompt via stdin
Avoid Windows ENAMETOOLONG by keeping Copilot prompts out of argv and sending them through stdin instead.\n\nFixes #705 |
||
|
|
f45055a8bf
|
docs+web: surface @nexudotio X account in README + entry sidebar (#696)
* docs(readme): add @nexudotio X link + Stay in the loop section - Add X/Twitter follow badge in the header next to Discord, so visitors can subscribe to release notes and milestone threads in one click. - Add a short "Stay in the loop" section above "Star us" pointing to @nexudotio on X. Discord is for chat, X is for the public milestones (releases, new skills, new design systems). No other changes. * feat(web): add Follow @nexudotio pill to entry sidebar foot - Surface the official X account next to the Settings/Pet/Language pills so users can subscribe to release notes from inside the app, not only the README. - Uses the existing .foot-pill style (already supports <a>, has text-decoration: none) and the existing 'external-link' Icon — no new CSS, no new icons, no i18n key required (single short English label). - Opens in a new tab with rel="noreferrer noopener". Pairs with the README badge added in this same PR. --------- Co-authored-by: Tuola-waj <ge@nexu.io> |
||
|
|
e6e5928be1
|
feat(web): add connection tests for execution settings (#507)
* feat(settings): add connection test for providers and CLI agents Adds a "Test" action in the Settings dialog that verifies the configured provider (Anthropic/OpenAI/Azure/Google) or CLI agent without sending a real chat. Backed by a new daemon endpoint and shared contracts, with categorized inline statuses and i18n strings across all supported locales. * fix(settings): address connection test review feedback * fix(daemon): pass empty MCP servers for connection probes * fix(connection-test): address review blockers * fix(daemon): fail json stream runs on structured errors * fix(contracts): build connection test subpath export * Use draft CLI env in agent connection tests * fix(i18n): add fallback ids for new curated content |
||
|
|
832ea7d864
|
fix: batch of small bug fixes (#283, #275, #390) (#530)
* fix(web): add hover tooltips to Design Files action buttons (#283) The batch-download, select-all, and clear-selection buttons in DesignFilesPanel had no title attribute, so users hovering them saw no tooltip. The other action buttons (refresh, new sketch, paste, upload) already had titles. Added titles to the three missing ones using the existing translation keys, so hover behavior is consistent across the panel. Closes #283. * docs: point pi-ai links to pi-mono packages (#275) The pi project moved from a standalone repo to the pi-mono monorepo. The old URL https://github.com/mariozechner/pi-ai now 404s. Replaced both shapes of reference: - The reference-style [piai]: definition now points at https://github.com/badlogic/pi-mono/tree/main/packages/ai (the multi-provider LLM API package). - Inline links whose visible text is the CLI tool 'pi' or 'Pi' now point at https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent (the interactive coding-agent CLI), so a reader clicking 'pi' in the daemon-discovery section lands on the actual binary's docs. Affected: README.md and 10 translated READMEs, plus docs/spec.md, docs/architecture.md, docs/references.md, docs/roadmap.md. Closes #275. * fix(daemon): expand $HOME / ${HOME} in OD_DATA_DIR (#390) Some launchers (systemd unit files, NixOS modules, certain Docker entrypoints) pass OD_DATA_DIR with a literal '$HOME' or '${HOME}' because no shell ever expands them. resolveDataDir previously only handled '~/' shorthand, so '$HOME/.open-design' fell through to path.resolve(PROJECT_ROOT, '$HOME/.open-design') and produced paths like /opt/open-design/$HOME/.open-design. resolveDataDir now expands '~', '~/...', '$HOME', '$HOME/...', '${HOME}', and '${HOME}/...' to os.homedir() before the absolute / relative branch runs. Rebuilds via path.join so the platform separator is correct on Windows even when the input used forward slashes. Tests: 7 unit tests cover empty/undefined, '~', '~/...', '$HOME', '$HOME/...', '${HOME}/...', absolute paths, and relative paths. Closes #390. * fix(daemon): accept backslash separators + hermetic resolve-data-dir tests Round 1 review feedback on PR #530. The previous regex only matched forward-slash separators, so a Windows launcher passing OD_DATA_DIR=$HOME\.open-design or ${HOME}\.open-design fell through to path.resolve(projectRoot, ...) and produced a directory named $HOME or ~ under projectRoot. The regex now accepts both forward and back slashes for the home-prefix separator. The previous tests called the real resolveDataDir against literal ~/od-test, $HOME/od-test, etc., which created and write-checked directories under the developer's or CI runner's actual home. The tests now stub os.homedir() with vi.spyOn to a per-test mkdtemp directory and remove it in afterEach, so no test ever writes outside its own sandbox. Added explicit fixtures for the Windows backslash forms ($HOME\od-test, ${HOME}\od-test, ~\od-test) so launcher coverage stays cross-platform. 12/12 resolve-data-dir tests pass, daemon typecheck clean. * fix(docs,daemon): apply pi-mono links to README.es and await test cleanup Round 2 review feedback on PR #530. README.es.md was added in upstream #552 after my pi-mono link sweep landed, so the daemon-discovery paragraph (line 222), the [piai] reference (line 684), and the Pi table row (line 709) still pointed at the broken https://github.com/mariozechner/pi-ai URL. Applied the same replacements: the [piai] ref now points at packages/ai, and the inline Pi link now points at packages/coding-agent. Spanish readers get the same coverage as the other 11 locales. The absolute-path test in tests/resolve-data-dir.test.ts dropped its fixture via void rm(abs, ...), so a failed async removal could leak rdd-abs-* directories from the suite. The test is now async and awaits the rm in the finally block, matching the awaited cleanup in afterEach. 12/12 resolve-data-dir tests still pass, daemon typecheck clean. * fix(daemon): share $HOME expander between OD_DATA_DIR and OD_MEDIA_CONFIG_DIR Round 3 review feedback on PR #530. resolveDataDir (server.ts) now expands $HOME / ${HOME} / ~, but media-config.ts had its own resolveOverrideDir that only handled ~/. Because configFile() falls back to OD_DATA_DIR when OD_MEDIA_CONFIG_DIR is unset, setting OD_DATA_DIR=$HOME/.open-design split state: SQLite, projects, and artifacts went to the expanded path while media-config.json stayed under <projectRoot>/$HOME/.open-design. Stored provider keys then appeared missing on the next read. Extracted the home-prefix expansion into apps/daemon/src/home-expansion.ts so resolveDataDir and resolveOverrideDir share one resolver. Both now recognize ~ / $HOME / ${HOME} (bare tokens) and ~/, ~\, $HOME/, $HOME\, ${HOME}/, ${HOME}\ (prefix forms with either separator). Three new media-config routing tests cover the OD_DATA_DIR fallback for $HOME/..., ${HOME}/..., and the OD_MEDIA_CONFIG_DIR explicit-override $HOME/... case so the co-location guarantee is locked down by tests. Daemon typecheck clean. Tests pass on Linux CI; the existing pattern in the file uses process.env.HOME which os.homedir() reads on POSIX. Resolve-data-dir tests stay hermetic via vi.spyOn. * docs(daemon): media-config comments reflect full $HOME / ${HOME} expansion Round 3 review feedback on PR #530 (lefarcen, P3 non-blocking). The file-header and resolveOverrideDir() function comment said only ~/ expands. Updated both to mention the shared expandHomePrefix() helper and the full set of forms it handles (~, $HOME, ${HOME} with either separator), so a future reader does not need to chase the implementation to understand what env values are accepted. * test(daemon): stub os.homedir() in media-config routing tests Round 4 review feedback on PR #530. The new $HOME / ${HOME} routing tests relied on process.env.HOME being read by os.homedir(), which works on POSIX but is unreliable on Windows (Node prefers USERPROFILE / profile APIs there). The tests would expand to the real user home while fixtures were written under the per-test homeDir, causing platform-specific failures in the same area this PR is making cross-platform. The inner describe block now stubs os.homedir() via vi.spyOn to return the per-test homeDir, matching the pattern in resolve-data-dir.test.ts. Restored in afterEach. The four $HOME-form routing tests now pass on both POSIX and Windows. Daemon typecheck clean. The two OAuth fallback test failures unrelated to this change (real ChatGPT/Codex tokens in the local env) remain out-of-scope. * fix(i18n): drop duplicate uk.ts promptTemplates keys after rebase Upstream #674 added the same Ukrainian translations my earlier commit added. The rebase landed both copies; tsc rejects duplicate property names. Drop my copies so #674 (which is now upstream) is the single source for these keys. --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
6abd7676c8
|
fix(daemon): unbreak Claude Design ZIP import on Node 24 and raise file ceiling (#591)
* fix(daemon): unbreak Claude Design ZIP import on Node 24 and raise file ceiling
- Skip inflateRawSync when an entry's central-directory uncompressedSize is 0;
Node 24 rejects { maxOutputLength: 0 } with ERR_OUT_OF_RANGE, which silently
killed the entire import for any zip containing an empty file or a streaming
entry whose size is only present in the data descriptor.
- Raise MAX_FILES from 500 to 5000. Real-world design-system exports commonly
exceed 500 files; MAX_TOTAL_BYTES (100 MB) and MAX_FILE_BYTES (25 MB) already
cap pathological inputs.
- Add regression tests for both: zero-byte deflate entry, central directory
advertising uncompressedSize=0, and a 600-file zip.
Refs #590
* fix(daemon): preserve real payload when central uncompressedSize is 0
Reviewers correctly flagged that the previous Buffer.alloc(0) fast-path
trusted the central directory's uncompressedSize, which is unreliable for
streaming/data-descriptor zips: an entry whose central record reports 0
can still carry real deflated bytes. The earlier fix wrote empty files to
disk in that case, and the post-condition body.length !== uncompressedSize
check still passed because both sides were 0.
- Inflate streaming entries with maxOutputLength = MAX_FILE_BYTES when the
central directory advertises 0, so legitimate non-empty payloads decode
fully instead of being silently truncated.
- Move size enforcement post-decode: per-file and total-byte budgets are now
computed from the actual decoded length, and the strict equality check is
skipped when central was 0 (i.e., genuinely unknown).
- Keep the empty-deflate degenerate case (compressed.length === 0) safe by
short-circuiting before zlib instead of relying on uncompressedSize.
Tests:
- New: streaming-zip case with central uncompressedSize=0 + non-empty body
asserts the on-disk file matches the original bytes (would have been
silently truncated under the previous fix).
- New: oversized streaming entry (> MAX_FILE_BYTES) is still rejected even
though the central directory under-reports.
- The original 0-byte and >500-file regressions remain covered.
|
||
|
|
5a29fed7d3
|
fix(web): align design system default test fixture (#708) | ||
|
|
4c82e48e4f
|
fix web design system selection persistence (#621) | ||
|
|
1bd1f3a661
|
fix(daemon): surface OpenCode error frames + treat empty-output runs as failed (#700)
* fix(daemon): surface OpenCode error frames + treat empty-output runs as failed Closes #691. OpenCode runs would silently complete in ~3 seconds without producing any visible chat output and still be rendered as a successful turn — three independent bugs along the structured-stream path conspired to produce this silent-failure shape. ## Bug 1 — `apps/daemon/src/json-event-stream.ts:85-91` OpenCode emits structured error frames on stdout (e.g. provider auth failures, network errors, schema mismatches) and still exits 0. The parser was downgrading these to `{type: 'raw', line: ...}`, which the chat UI does not render as an assistant message. The error string was discarded as "no-op output." Fix: emit a proper `{type: 'error', message, raw}` event matching the qoder-stream contract that the daemon's existing error-handling path already recognises. ## Bug 2 — `apps/daemon/src/server.ts:4199-4205` Even after Bug 1 was fixed, the json-event-stream branch wired the parser to a bare `(ev) => send('agent', ev)` lambda — bypassing the `sendAgentEvent` wrapper that interprets `type:'error'` events and sets the `agentStreamError` flag the close handler reads to flip the run to `failed`. So an emitted `error` event would just be forwarded as a no-op `agent` SSE event with no lifecycle effect. Fix: route json-event-stream through `sendAgentEvent`, mirroring the qoder-stream-json wiring at line 4175. ## Bug 3 — `apps/daemon/src/server.ts:4220-4234` Even after Bugs 1 and 2 are fixed, there's still a class of runs where OpenCode never emits any error frame, never emits any substantive event, and exits 0. Pre-fix this was marked `succeeded` and the user saw a blank chat with no diagnostic. Fix: track `agentProducedOutput` inside `sendAgentEvent` (set on `text_delta`, `thinking_delta`, `tool_use`, `tool_result`, `artifact` — deliberately NOT on `status` / `usage`, since a model can emit token-usage numbers for an empty completion). When the close handler sees `code === 0 && trackingSubstantiveOutput && !agentProducedOutput` the run is marked `failed` with an explicit AGENT_EXECUTION_FAILED SSE error so the chat shows a clear reason instead of a silent empty turn. The check is gated by `trackingSubstantiveOutput` so it only fires on streams that actually contribute to the output flag (currently qoder-stream-json and json-event-stream). ACP sessions and plain stdout streams keep their existing success/failure determination. ## Tests - 3 new unit tests in `apps/daemon/tests/json-event-stream.test.ts` pin the OpenCode error event shape: full repro (`error.data.message`), `error.name` fallback, and the generic-fallback shape when `error` is empty. - All 60 daemon test files (851 tests) pass on `pnpm --filter @open-design/daemon test`. All 42 web test files (309 tests) pass on `pnpm --filter @open-design/web test`. - Full repo `pnpm typecheck` clean. ## Live verification Verified end-to-end via a stub `opencode` binary that mimics each of the failure shapes against `pnpm tools-dev run web`: 1. Stub emits `{"type":"error",...}` then `exit 0` — run now ends as `failed` with the OpenCode error message surfaced as an SSE `error` event. Pre-fix this was `succeeded` with an empty chat. 2. Stub emits nothing then `exit 0` — run now ends as `failed` with "Agent completed without producing any output…" diagnostic. Pre-fix this was `succeeded` with an empty chat. 3. Stub emits a normal `step_start` / `text` / `step_finish` sequence then `exit 0` — run still succeeds. (Regression check.) ## Out of scope (mentioned for the next person) - `claude-stream-json` and `copilot-stream-json` still wire to a bare `(ev) => send('agent', ev)` and don't currently parse `type:'error'` frames. If their CLIs ever start emitting structured error events the same pattern (route through `sendAgentEvent` + emit proper `type:'error'`) applies. Not in scope here because we have no evidence those CLIs do this today, and changing the wiring without a confirmed failure mode risks regressing currently-working flows. - ACP sessions (`pi-rpc`, `acp-json-rpc`) own their own success / failure determination via `acpSession?.hasFatalError()` and the empty-output guard explicitly skips them via `trackingSubstantiveOutput`. - Plain stdout streams have no event-level tracking, so the empty- output guard skips them too. Diagnosing a no-output plain-stream agent is a separate problem that needs different signals. * chore: retrigger CI on top of green main (post #697 i18n backfill) |
||
|
|
4368b8f163
|
feat(linux): add headless mode for install/start/stop operations (#686)
* feat(linux): add headless mode for install/start/stop operations * docs(linux): document headless mode commands and usage * refactor(linux-headless): write web-root.json instead of polling IPC for URL * fix(linux-headless): fail start when web identity never appears instead of returning success * docs(linux-headless): add use-case context and clarify launcher path dependency * fix(linux-headless): ensure launcher, identity and shutdown align with tools-pack - Bake OD_DATA_DIR into launcher so manual runs use the same paths as tools-pack - Validate web-root.json fields before accepting to reject stale identity - Remove web-root.json on successful stop - Add IPC server for graceful STATUS/SHUTDOWN handling * fix(linux-headless): create IPC server before writing web-root.json |
||
|
|
a5aaeb7905
|
Fix: Remove element selector tooltip in Tweaks mode (#697)
* Fix: Remove element selector tooltip in Tweaks mode * fix: cover localized content fallbacks * fix: keep comment target labels testable --------- Co-authored-by: bojiehuang <bojiehuang@bojiehuangdeMacBook-Pro.local> Co-authored-by: mrcfps <mrc@powerformer.com> |
||
|
|
576dfed9e1
|
feat: add accent color control and launcher for Open Design (#683)
* feat: add accent color control and launcher for Open Design * fix: remove launcher binary from PR * test: cover accent appearance edge cases --------- Co-authored-by: ferasbusiness666 <ferasbusiness666@users.noreply.github.com> |
||
|
|
42e4d080bd
|
feat(skills): add social-media-dashboard skill + Totality Festival design system (#678)
* feat(skills): add social-media-dashboard skill + Totality Festival design system - New skill 'social-media-dashboard': single-screen creator analytics dashboard with platform switcher (X / GitHub / LinkedIn / YouTube / Instagram), KPI row, growth chart with annotations, top-post / top-PR preview, trending topics, and top comments. Includes a self-contained example.html (Totality Festival styled, X + GitHub tabs, live KPI ticker, GitHub contributors grid, world-map audience geography). - New design system 'totality-festival': cosmic-premium dark glassmorphic system with amber corona highlights and cyan atmospheric accents. Mirrors Google Labs' design.md spec example so skills can be validated against an upstream reference. - Fix swatches parser in apps/daemon/src/design-systems.ts so it recognises the '- **Name:**' bold-with-inner-colon form used by several existing systems (ant, totality-festival, ...). Previously only the '**Name** (`#hex`)' form was matched, which left their picker thumbnails empty. * feat(skills): polish social-media-dashboard example + add PR preview - Top Post media block: replace empty gold frame with an inline SVG thumbnail (radial glow + ascending data curve + amber/cyan pulse dots + play icon + 'LIVE · 0:22' caption). Visually echoes the live-artifact story the post copy is selling. - Hoist the brand-mark linearGradient into a global SVG defs block at the top of <body> so all three avatars (header, user, top-post) can reference url(#brandRing) and render the teal arrow consistently. Previously only the header SVG carried the gradient definition, so the user and post avatars rendered as empty rings under headless capture. - Add hero.png preview to .preview/ for the PR description. --------- Co-authored-by: Tuola Ge <gexingli@refly.ai> |
||
|
|
98e40c1cfc
|
feat(daemon): export project transcript to disk for downstream consumption (prereq for #450) (#493)
* feat(daemon): export project transcript to disk for downstream consumption
Adds exportProjectTranscript(db, projectsRoot, projectId, options?) — a
pure function that walks SQLite-backed conversation history and writes a
structured, lossless JSONL transcript to <projectDir>/.transcript.jsonl.
This is the input primitive that #450's "Finalize design package"
synthesis step needs. Landed ahead of the synthesis endpoint as a small,
reviewable, well-tested unit — no HTTP route, no LLM call, no web UI in
this diff. PR 2 will wire POST /api/projects/:id/finalize on top of it.
Format: JSONL with header line + per-conversation marker lines +
per-message lines. Compact encoding saves ~20–30% on synth-call tokens
vs pretty-printed JSON. schemaVersion field reserved on the header for
incompatible changes later.
Coalescing: events_json carries streaming text_delta / thinking_delta
chunks plus tool_use / tool_result / thinking_start markers and
telemetry. The export collapses runs of same-type deltas into terminal
text / thinking blocks via arrival-order with type-change flush,
preserving any interleaving with tool blocks. Telemetry (status, usage,
raw) is dropped. thinking_start is treated as an explicit flush trigger
so multiple thinking blocks in one message survive intact.
Content fallback: user-typed messages persist as plain text in
messages.content with events_json = NULL because user input does not
flow through the streaming pipeline. When event-derived blocks come
back empty, fall back to a single text block from content so user
prompts are not silently dropped.
Atomic write: tmp filename includes pid + crypto-random suffix so
concurrent exports for the same project cannot collide. fsync before
rename so a crash between rename and power loss cannot lose bytes.
Hidden file: leading dot keeps .transcript.jsonl out of listFiles
(projects.ts:54), the archive zip, and the gallery.
Tests: 14 unit cases — empty project, text/streaming coalescing, tool
ordering, telemetry filtering, type-change flush, text↔thinking↔text
interleaving, thinking_start flush, multi-conversation chronological
order, atomic-write hygiene, content fallback (both directions),
malformed events_json, and unsafe project id rejection.
Refs: nexu-io/open-design#450
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(daemon): address PR #493 review feedback for transcript export
Addresses every blocker, P2, and P3 raised on
https://github.com/nexu-io/open-design/pull/493:
- Blocker (event-shape mismatch): coalescer now switches on the
PersistedAgentEvent kind discriminator (text/thinking/tool_use/
tool_result), reading the shared `text` field for content kinds,
matching what apps/web/src/providers/daemon.ts:347-394 actually
persists into messages.events_json. Empirical confirmation: live
SQLite contains zero `type` keys.
- Removed @ts-nocheck from the source file; added inline types for
Db (Database.Database), ConversationRow, MessageRow, AttachmentRef,
CommentAttachmentRef, and Block. Tests retain @ts-nocheck per
codebase convention. Note: db.ts still uses @ts-nocheck, so the
new types catch drift inside transcript-export.ts itself, not at
the SQLite-helper boundary.
- parseEvents now distinguishes null / malformed / not_array / ok
cases; non-null-but-unparseable rows emit a console.warn with
project+message id before falling back to content.
- Switched temp-write from writeSync (which can return short) to
writeFileSync({flag:'wx'}); explicit fsync via reopen before
rename, per reviewer concern about partial-write durability.
- Added per-project lockfile (.transcript.lock) acquired with
openSync(..., 'wx') and released in finally; concurrent exports
throw the new TranscriptExportLockedError. Stale-lock recovery
is documented as a known limitation in the file header.
- Header gains attachmentCount, commentAttachmentCount, and explicit
attachmentsInlined: false. Per-message lines gain attachments /
commentAttachments references (paths only, not bytes; synthesis
reads files from disk by path). schemaVersion bumped 1 -> 2 so
the change is explicit; v1 was never consumed.
- mkdirSync(dir, { recursive: true }) at entry covers projects
with DB rows but no on-disk directory yet (codex bot finding).
- Refactored node:fs imports from named to default
(import fs from 'node:fs') so vitest spies in tests #15-#17 can
redefine properties on the underlying CJS exports object. ESM
namespace imports of node:fs produce a frozen Module Namespace
Object that vi.spyOn cannot mutate; default-import returns the
CJS module.exports which is mutable.
- Inline PersistedAgentEvent union: the daemon tsconfig does not
resolve the `@open-design/contracts/api/chat` subpath export, so
the union is restated in the source. Schema-mismatch tests cover
the case where the contract would diverge.
- Test count 14 -> 24: failure injection for writeFileSync /
fsyncSync / renameSync, existing-file replacement, lockfile
contention (lockfile-pre-create design — synchronous API can't
race via Promise.allSettled), parse-warning cases (malformed +
not-array), attachments header + per-message coverage, missing-
project-dir case.
Refs nexu-io/open-design#450 (does not close).
* fix(daemon): preserve thinking-segment boundaries on status thinking-start
Codex flagged this as a P2 on PR #493
|
||
|
|
8301bcd46e
|
test: add desktop settings and project flow e2e coverage (#306)
* test: add desktop settings regression coverage * test: stabilize desktop smoke interactions on latest main * fixer: address PR #306 follow-up items Generated-By: looper 0.2.7 (runner=fixer, agent=codex) * test: expand ui e2e automation suite * fix: add missing Ukrainian prompt template labels * chore: align desktop e2e helpers with layout guard * chore: move settings protocol e2e into ui suite * fix: preserve api provider settings across protocol switches * fix: avoid leaking api keys across protocol drafts * test: fold desktop smoke coverage into mac spec * fix: dedupe Ukrainian prompt template labels |
||
|
|
3298cb3756
|
feat(skills): add 5 Orbit briefing templates (#671)
* feat(skills): add 5 Orbit briefing templates
Introduces a new "orbit" scenario family in the Examples gallery for
morning-briefing surfaces. Each template lives at the top of "我的设计"
and aggregates yesterday's connector activity into a single page.
- orbit-general: adaptive bento dashboard that fans across 12-16
connectors, where each module picks its own UI form by data type
(list / avatar stack / status ring / heatmap / file grid / alert
card / kanban / etc.)
- orbit-github: GitHub-flavored single-connector digest mirroring the
Notifications + PR-diff visual language
- orbit-gmail: Gmail-flavored digest rendered as a Daily Digest email
inside the three-pane inbox
- orbit-linear: Linear-flavored digest in the dark Inbox + cycle-
progress layout
- orbit-notion: Notion-flavored digest authored as a native Notion
page (callout / toggle / database table)
The new scenario value 'orbit' surfaces as a filter pill in
ExamplesTab automatically; no UI code change required.
* fix(skills): reframe Orbit skill descriptions as pipeline-triggered
The original descriptions framed each skill as a standalone "X-flavored
briefing template" the user picks. They are actually skills the Orbit
daily-digest pipeline selects automatically based on which connectors
the user has authenticated, then runs against live connector data.
Rewrites both `description` and `example_prompt` for all 5 templates:
- orbit-general: invoked when 2+ connectors are connected; aggregates
the past 24h across every authenticated source
- orbit-github / orbit-gmail / orbit-linear / orbit-notion: invoked
when the named connector is the user's only connection (or scope is
explicitly limited to it); pulls the past 24h from that connection
alone
All 5 now state explicitly that they are not user-triggered — the
Orbit scheduler invokes them.
* feat(examples): add Orbit pill to the mode filter row
Surfaces the Orbit briefing skills as a top-level "type" filter in the
Examples gallery, alongside Prototype · Desktop / Mobile / Slide deck /
Docs & templates. Filter matches skills with scenario === 'orbit'.
- ExamplesTab: extend ModeFilter and MODE_PILLS with 'orbit'; teach
matchesMode and modeCounts about it
- i18n: add 'examples.modeOrbit' to Dict and to all 16 locale files
('Orbit' is left untranslated as a brand name)
* polish(orbit-general): real Figma preview image + revised comment
Replaces the empty gray placeholder in the Figma module with an
Unsplash UI-design photo, and rewrites the mock comment to read like
a substantive design-review note rather than a nit about button
placement.
* feat(examples): eager-load card previews via IntersectionObserver
Card previews previously only loaded on hover, leaving the example
gallery showing 'Hover to preview' placeholders for everything below
the fold. Now each card observes the viewport and prefetches its HTML
800px before scrolling into view, so the iframe is ready by the time
the user reaches it.
Hover remains as a fallback path (and for browsers without
IntersectionObserver, the card loads immediately on mount).
Also reverts the Unsplash photo on the orbit-general Figma module
back to the gray placeholder — the stock image semantically misread
as a Photoshop screenshot rather than a Figma artboard.
* feat(orbit-general): drop Figma connector module
Removes the Figma bento card and its scoped CSS, plus the orphaned
Top-3 entry that referenced a Figma comment. Reassigns Top-3 #2 to
a Notion document review so the priority list stays aligned with
the connectors actually rendered.
* i18n(skills): translate Orbit example prompts to English
The example_prompt is what gets injected into the chat input when a
user clicks 'Use this prompt', and is read by the agent verbatim. It
should match the SKILL.md description language (English), not the UI
locale. Replaces the Chinese drafts with English equivalents across
all 5 Orbit skills, and drops the Figma reference from orbit-general
since that connector module was removed earlier.
* fix(skills): rewrite Orbit SKILL.md bodies with reproducible specs
Earlier the bodies were too abstract (only a connector→UI mapping
table and a one-line style note), so agents running the skill could
not reproduce the shipped example.html and got stuck in long retries.
Each SKILL.md body now contains:
- exact color tokens lifted from the example.html
- type stack and font sizes
- a section-by-section page spec (top-to-bottom)
- chip / pill / icon rendering rules
- forbidden list
The example_prompt is collapsed back to a one-line user intent so the
skill body is the source of design truth.
Covers all 5 templates: orbit-general, orbit-github, orbit-gmail,
orbit-linear, orbit-notion.
* feat(orbit): make every connector item clickable
Each Orbit briefing template now links its rows / cards to the matching
source URL so users can jump straight from the morning digest to the
underlying connector.
- orbit-general: each bento card gains an 'Open in {connector} ↗' CTA
built from a connector→URL map; each Top 3 card becomes an anchor
- orbit-github: every event row opens the corresponding github.com
pull/issue URL parsed from the row identifier; the header logo links
to the repo
- orbit-linear: each issue row gains a small ↗ button that opens
linear.app/{team}/issue/{ID}
- orbit-gmail: action and reply buttons jump to a Gmail search URL
scoped to the sender
- orbit-notion: page-link spans wrap as anchors and database rows are
click-to-open against notion.so
All links use target="_blank" rel="noopener noreferrer".
* fix(skills): force agents to mirror example.html 1:1
Earlier skills told the agent the example was 'source of truth' but
left phrasing soft, so agents felt free to add extra UI elements
(snoozed-mail row, extra yellow stars on inbox rows, etc.) that
were not in example.html.
Each Orbit SKILL.md now opens with a 'Source-of-truth protocol' that
forces the agent to:
1. read example.html before writing any output
2. mirror its DOM structure / class names / module count / element
order 1:1
3. only refresh mock values; never invent additional UI elements,
rail entries, sections, badges, or chrome ornaments
The reference sections that follow stay informative for tokens and
visual language but are explicitly demoted from spec to commentary.
* fix(orbit-gmail): remove three-pane / left-rail / inbox-list claims
The example.html is a single-column page: Gmail top header + the
opened Orbit Daily Digest email (toolbar / subject / sender / digest
body / reply bar). Earlier copy described a Gmail three-pane app with
Compose button, label list, Categories tabs, and an inbox listing —
none of which exist in the actual asset.
- example_prompt: drops 'three-pane inbox' phrasing
- description: same
- body: rewrites Page sections to mirror the real header → email-chrome
layout, top to bottom; explicitly forbids left rail, inbox list, and
Categories tab strip
* feat(orbit): forbid external design systems in all 5 skills
Each Orbit briefing template ships its own complete visual language
baked into example.html (Gmail / GitHub / Linear / Notion / Open
Orbit's editorial bento). Adds an explicit 'Design system policy'
block telling the agent to:
- ignore any DESIGN.md attached to the active project
- ignore brand tokens or Figma files supplied via chat
- use exclusively the colors / fonts / radii from example.html
This is a hard constraint: an Orbit briefing must look like the
connector it represents, not like the user's brand.
* feat(newproj): hide design-system picker for skills that opt out
Skills can declare 'od.design_system.requires: false' in SKILL.md to
opt out of DESIGN.md injection (the Orbit briefing skills do this —
their example.html ships with a complete connector-native visual
language). When the active default skill for a tab opts out, hide the
design-system picker so we don't ask the user to attach a brand we'll
then ignore.
Existing tabs that don't host a default skill (template, other) keep
the picker. The check only fires for prototype / live-artifact / deck.
* review: address P2 reviewer feedback
P2 — Connector family coverage gaps (orbit-general):
Adds Finance, CRM/Sales, Support, Analytics, Infrastructure, Security
rows to the connector→UI mapping table (now 16 families). Adds a
'Fallback heuristics' subsection so unknown connectors are routed by
data shape (numbers + time series → Alerts, rows + status field →
Task mgmt, etc.).
P2 — 'Forbidden' rules too vague (all 5 skills):
Rewrites every Forbidden section as a paired 'Don't / Do' constraints
table so each negative is paired with a concrete positive. Replaces
obvious bans (lorem ipsum) with substantive ones (real-shaped mock
copy, plausible identifiers, dev-team label hues, etc.).
* ci: register orbit skills in de/ru/fr en-fallback lists
The localized-content coverage test asserts that every skill in
skills/ is either translated or explicitly declared as falling back
to English in the LOCALIZED_CONTENT_IDS bundle. The 5 new orbit
skills weren't in any bundle, so the workspace validation job failed
on the de/ru/fr coverage assertions.
Adds the 5 orbit-* ids to DE/FR/RU_SKILL_IDS_WITH_EN_FALLBACK so
those locales explicitly fall back to the SKILL.md English copy
(matching the minimal-change posture chosen earlier in this PR).
|
||
|
|
2455c70d51
|
fix(daemon, packaged): unbreak GUI-launched agent detection on minimal PATHs (#442) (#614)
* fix(daemon, packaged): unbreak GUI-launched agent detection on minimal PATHs (#442) GUI-launched daemons (Finder/Dock on macOS, .desktop on Linux) inherit a stripped PATH from launchd / the desktop session and don't read the user's interactive shell rc files, so any CLI installed via `npm i -g` under a sudo-free prefix like ~/.npm-global was silently undetected. Two layers maintained their own copies of the user-toolchain bin list (`apps/daemon/src/agents.ts:userToolchainDirs` for the resolver, `apps/packaged/src/sidecars.ts:resolvePackagedPathEnv` for the packaged sidecar PATH builder) and had already drifted on `~/.asdf/shims` and `~/Library/pnpm`. Adding ~/.npm-global to one side would have preserved the same anti-pattern. Extracts `wellKnownUserToolchainBins` into @open-design/platform as the single source of truth, has both layers consume it, and extends the list to cover ~/.npm-global/bin, ~/.npm-packages/bin, plus $NPM_CONFIG_PREFIX/bin / $npm_config_prefix/bin for users with a non-standard prefix. New vitest coverage in the platform package and a regression test in apps/daemon/tests/agents.test.ts modelled on the existing mise case. Verified end-to-end: under PATH=/usr/bin:/bin:/usr/sbin:/sbin (the launchd default a `.app` actually inherits), `resolveAgentExecutable` now returns ~/.npm-global/bin/gemini instead of null. * fix(daemon): isolate OD_AGENT_HOME resolution from $NPM_CONFIG_PREFIX leakage Address review feedback on PR #614: - mrcfps spotted that the daemon wrapper called wellKnownUserToolchainBins without passing `env`, so the helper read its default process.env. A developer or CI runner with NPM_CONFIG_PREFIX / npm_config_prefix exported would inject that real <prefix>/bin into resolveOnPath() even while the OD_AGENT_HOME hook pointed home at a temp fixture, making agent-detection tests environment-dependent. Reproduced locally: with OD_AGENT_HOME=<tmp> + NPM_CONFIG_PREFIX=/Users/me/.npm-global, resolveAgentExecutable({ bin: 'codex' }) returned the real machine's binary instead of null. Wrapper now passes `env: {}` whenever homeOverride is set, alongside the existing includeSystemBins gate. - lefarcen suggested also handling whitespace-only NPM_CONFIG_PREFIX values (e.g. NPM_CONFIG_PREFIX=" ") so the helper does not emit a bogus "<whitespace>/bin" entry. Added a .trim() check before appending. - lefarcen also suggested a comment pointer from the daemon wrapper to the platform helper so readers don't have to grep. Added the reference inline. Coverage: - packages/platform/tests/index.test.ts: new whitespace-prefix case. - apps/daemon/tests/agents.test.ts: new env-isolation regression asserting that OD_AGENT_HOME + NPM_CONFIG_PREFIX cannot leak the real prefix bin into the sandbox. * test(daemon): preserve $NPM_CONFIG_PREFIX across the env-isolation case (#614) Address mrcfps's second-round review on PR #614: the env-isolation regression sets `process.env.NPM_CONFIG_PREFIX = realPrefix` in its body and then unconditionally `delete`s it in `finally`. On a developer machine or CI runner that already exported `NPM_CONFIG_PREFIX`, that mutates the worker-wide env for every later test, making downstream env-sensitive assertions order-dependent. Move the save/restore into the file's existing afterEach hook (mirroring the OD_AGENT_HOME / OD_DAEMON_URL / OD_TOOL_TOKEN pattern) and drop the in-test `delete`. Same coverage, no worker-state mutation. * fix(platform): prioritise $NPM_CONFIG_PREFIX over the conventional npm guesses (#614) Address mrcfps's third-round review on PR #614: when the user has explicitly configured a prefix via $NPM_CONFIG_PREFIX (or $npm_config_prefix), that's where `npm i -g` puts the *current* binaries. The conventional guesses ~/.npm-global / ~/.npm-packages often hold *stale* installs from an older prefix the user has since rewritten — searching the env-driven prefix first matches npm's own resolution order (env > .npmrc > default) and gives "explicit beats convention" semantics. Move the env-driven push above the conventional `dirs.push(.npm-global, .npm-packages)`. Add a vitest case in the platform package that asserts $NPM_CONFIG_PREFIX/bin's index in the result is strictly less than ~/.npm-global/bin's and ~/.npm-packages/bin's. `resolveOnPath()` and the packaged PATH builder both preserve insertion order, so first hit wins and the new ordering propagates to both layers. * fix(platform): lift $NPM_CONFIG_PREFIX above every conventional bin (#614) Address mrcfps's fourth-round review on PR #614: the previous fix only moved $NPM_CONFIG_PREFIX/bin ahead of ~/.npm-global / ~/.npm-packages, but ~/.local/bin still appeared earlier in the array. Under a minimal GUI-launch PATH a stale agent in ~/.local/bin (also a shared dumping ground for pip --user / cargo install / hand-built binaries) could outrank the user's *current* explicit npm prefix. Move the env-driven push to the head of `dirs` so the explicit prefix wins over every conventional location below — ~/.local/bin included. Matches npm's own resolution order (env > .npmrc > default) across the whole list, not just the npm-prefix block. Tightened the existing order test to assert `explicitIdx === 0` and that ~/.local/bin's index is strictly greater than the explicit prefix's index, so a future drift would fail loudly. |
||
|
|
f3024fdc22
|
feat(media): add Nano Banana image provider (#631)
* feat(media): add Nano Banana image provider * fix(media): support Gemini API key headers for Nano Banana * refactor(media): move Nano Banana model override flag into provider metadata |
||
|
|
8dc0875147
|
fix(i18n): remove duplicate Ukrainian prompt template keys (#680) |