mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix/codex-cli-path-fallback-ux
532
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
42ae1da03d | Add Turkish README translation (#843) | ||
|
|
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> |
||
|
|
9674f48f2f |
fix(postinstall): auto-rebuild better-sqlite3 on Node.js ABI mismatch (#813)
* fix(postinstall): auto-rebuild better-sqlite3 on Node.js ABI mismatch prebuild-install fetches a prebuilt binary for the Node.js version active at install time. On systems where the Node ABI differs from Node 24 (e.g. Arch Linux system Node, Node 22 LTS, Node 25), or after switching versions, the addon fails to dlopen at daemon startup. postinstall now tries to load the native addon after the workspace builds. On failure it locates node-gyp from the pnpm virtual store (bundled with better-sqlite3) and rebuilds from source — no external tooling beyond a C++ compiler required. pnpm install becomes self-healing across Node versions. Also adds a QUICKSTART troubleshooting entry for users with ignore-scripts=true who need to run `node scripts/postinstall.mjs` manually. * fix(postinstall): correct better-sqlite3 path and rebuild mechanism Two bugs in the initial implementation caught in review: - better-sqlite3 is declared by apps/daemon, not the workspace root. node_modules/better-sqlite3 at root does not exist in a normal pnpm install, so existsSync() was always false and the check never ran. Fix: resolve via createRequire from apps/daemon/package.json. - better-sqlite3@12.9.0 depends only on bindings and prebuild-install, not node-gyp. The assumed sibling path in the pnpm store does not exist, so the rebuild branch was hitting the "not found" exit instead of rebuilding. Fix: use pnpm --filter @open-design/daemon rebuild better-sqlite3 so pnpm manages node-gyp through its own lifecycle. Also expands the QUICKSTART troubleshooting entry with the manual rebuild command, a verification step, and build tool prerequisites. * fix(quickstart): scope better-sqlite3 verification to daemon package |
||
|
|
ef4e673ad0 |
docs(readme): refresh contributors wall (#856)
Co-authored-by: mrcfps <23410977+mrcfps@users.noreply.github.com> |
||
|
|
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> |
||
|
|
6e473a4f77 |
feat(skills): teach hyperframes skill the HTML-in-Canvas API (#852)
* feat(skills): teach hyperframes skill the HTML-in-Canvas API Vendored skill predates upstream v0.5.1, which added the drawElementImage HTML-in-Canvas guide and the vfx-iphone-device / vfx-liquid-glass / vfx-portal catalog blocks. Without that reference, agents asked to build 'live HTML on a 3D phone screen' compositions had no idea the API existed and produced renders where the device screen was blank or static. - Add references/html-in-canvas.md adapted from the upstream guide, with an OD-specific note about render-loop re-capture (the most common reason output 'looks dead' inside a generated composition). - Cross-link the new reference from SKILL.md and add triggers for 'html in canvas', 'drawElementImage', 'html shader', and the two most-requested vfx blocks. Daemon render path is unchanged: 'npx hyperframes render' already auto-enables --enable-features=CanvasDrawElement, and we always pull the latest published hyperframes via npx, so no version pinning needs to move. * docs(skills): wait for canvas paint in hyperframes HTML-in-Canvas examples The drawElementImage API only refreshes its element snapshot when the canvas paints. Calling it during initial script evaluation can throw because no snapshot exists yet, and calling it later from outside a paint event silently reads the previous snapshot. On HyperFrames' seek-driven renders that surfaces as a failed or stale first texture. - Drive the basic capture example from canvas.onpaint and kick it off with requestPaint() instead of calling drawElementImage at script eval time. - Rewrite the per-frame re-capture pattern to put drawElementImage inside onpaint and call requestPaint() from the render loop, so each frame sees a fresh snapshot rather than the previous one. - Add a callout explaining the paint-event requirement so agents do not regress to the script-eval-time pattern. Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code) * docs(skills): add vfx-portal trigger to hyperframes skill The hyperframes skill's reference callout already names vfx-portal alongside vfx-iphone-device and vfx-liquid-glass as effects that should auto-load references/html-in-canvas.md, but the triggers list only includes the other two. A prompt like "make a vfx-portal clip" therefore misses the HTML-in-Canvas guidance the new reference adds. - Add "vfx-portal" to triggers so the trigger surface matches the documented entry points. Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code) |
||
|
|
32df17b87b |
Fix desktop preview interactions and connector auth feedback (#864)
* Fix desktop preview modal interactions * Fix connector auth failures surfacing |
||
|
|
915c041545 |
Update docs/assets/github-metrics.svg - [Skip GitHub Action] (#853)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
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> |
||
|
|
294fe94c67 |
fix(pack/win): close detection gaps that let Open Design.exe stay locked at install time (#821) (#823)
The custom NSIS pre-install flow detects and closes running OD processes before extraction, but two gaps let `$INSTDIR\Open Design.exe` stay locked when the installer reaches `MUI_PAGE_INSTFILES`. The user then sees NSIS's native "file in use" Retry/Cancel dialog (not the custom `RunningInstancesCloseFailed` text), which is what kutzki reported. `DetectRunningInstances` and `CloseRunningInstances` previously matched processes only by `Win32_Process.ExecutablePath` under the install root. WMI returns null `ExecutablePath` for processes the caller cannot fully introspect: insufficient access tokens, processes mid-spawn, protected-process states. A child spawned in the millisecond window between the previous OD running and the installer's detection step can hit this and slip past the filter. Both functions now fall back to a CommandLine prefix match against the install root for null- `ExecutablePath` rows, which is OD-specific enough to avoid false positives without relying on a global `Name` match. `CloseRunningInstances` previously called `Stop-Process -Force` and returned without waiting for the OS to actually finalize the process exit. On Windows the file handle GC for an exiting process is async, so a `MUI_PAGE_INSTFILES` overwrite right after the kill can race the handle release and trigger NSIS's native file-in-use prompt even though the kill succeeded. The function now `WaitForExit(5000)` per PID after the force-stop loop, before returning, so the lock has time to clear before NSIS attempts the overwrite. Both changes were endorsed by @lefarcen in the issue thread after they ran their own code review and confirmed the matching diagnosis. The third part of the proposed fix (cross-platform `before-quit` cleanup in the Electron app) is in scope for #422 and not touched here. Local validation: `pnpm guard` clean. `pnpm --filter @open-design/tools-pack typecheck` fails on a pre-existing issue (missing `@electron/rebuild` devDep in tools-pack/src/win/app.ts on current main, reproducible by checking out main directly without my edit), unrelated to this change. The PowerShell embedded in the NSIS template is not exercised by the workspace test suite, so the change has no unit-test surface. Honest caveat: I do not have a Windows packaged-build environment to run `pnpm tools-pack win build --to nsis` and reproduce the locked-file dialog end-to-end. The PowerShell edits are textual and match the patterns already in the file, but a verifying install pass on a real Windows host with a previous OD already installed and running is recommended before merge. 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> |
||
|
|
a383b4bd3a |
Preserve beta e2e spec reports in R2 (#812)
* Upload beta e2e spec reports to R2 * Expose beta report URLs in summary * Complete Indonesian deploy locale keys |
||
|
|
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. |
||
|
|
bc9a49ff48 |
craft: add laws-of-ux guidance
Adds the laws-of-ux craft guidance for generated UI work. |
||
|
|
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. |
||
|
|
2bbd677ea2 |
add clinic-console live-artifact template (#795)
* add clinic-console live-artifact template
Adds a built-in clinic-console template under
skills/live-artifact/assets/templates/, fulfilling the
assets/templates/<name> directory shape that
specs/2026-04-29-live-artifacts/spec.md §5.1 already plans for but
has not yet populated.
Three files, no other changes:
- template.html — html_template_v1 source, only DOM, CSS tokens, and {{data.*}} bindings
- data.json — canonical default sample (~6.7 KB, well within bounded JSON limits)
- README.md — data contract + telemedicine/pharmacy/pediatric variants
Verified locally against apps/daemon/src/live-artifacts/render.ts:
all paths match the html_template_v1 grammar, all bindings resolve
to scalars, no script/iframe/srcdoc/on*=/javascript:/raw HTML
directives, no forbidden JSON keys, JSON within bounded envelope.
* clinic-console: hardcode icon refs, drop icon_href bindings
Address @mrcfps's review on PR #795: stop interpolating {{data.*}} into
<use href="..."> attributes. The html_template_v1 binding contract
forbids interpolation inside URL-bearing attributes, and the security
validator runs *before* {{data.*}} substitution — so even a benign
icon_href today could be replaced with javascript:alert(1) tomorrow
without the validator ever seeing it.
Fix:
- template.html: every <use href="..."> is now a hardcoded literal
(#icon-dashboard, #icon-message, …). 14 nav slots + 4 KPI tiles
converted; total <use href="..."> count unchanged at 28, all
fragment-id literals, zero {{data.*}} inside URL-bearing attrs.
- data.json: icon_href removed from each nav_main[] item, each
nav_management[] item, and from kpi_a / kpi_b / kpi_c / kpi_d
(14 keys removed). Sample is now 6,333 bytes (was ~6,840).
- README.md: data-contract tables no longer list icon_href; new
"Icons are template-locked" section explains the binding-contract
rationale, lists the hardcoded id per slot, and gives guidance for
future runtime-configurable icons (route through a constrained,
non-URL mechanism such as enumerated CSS classes — never interpolate
into <use href> directly).
Verified locally against apps/daemon/src/live-artifacts/render.ts:
298 bindings, 297 unique paths, all resolve to scalars; zero
{{...}} inside any URL-bearing attribute (href / src / action /
formaction / srcset / xlink:href / ping / background / poster /
cite); none of the 6 forbidden security patterns present; bounded
JSON envelope: 6.3 KiB / depth 5 / max 22 keys / max 35 array items
/ max 45-char string — all well within limits.
---------
Co-authored-by: Joey-nexu <236967869+joeylee12629-star@users.noreply.github.com>
|
||
|
|
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> |