Commit graph

305 commits

Author SHA1 Message Date
Nagendhra Madishetti
3c94fa575c
feat(daemon): expose gemini-3 preview models and 2.5-flash-lite in the Gemini picker (#986)
* feat(daemon): expose gemini-3 preview models and 2.5-flash-lite in the Gemini fallback list

Issue #981: the Gemini agent picker only offered 2.5 Pro / 2.5 Flash
even though the Gemini CLI ships gemini-3-pro-preview, gemini-3-flash-preview,
and gemini-2.5-flash-lite. Users had to fall back to "Default" or
type the id by hand to reach the newer generation.

Add the three model ids to fallbackModels with the same shape as the
existing entries so the picker surfaces them inline.

* test(daemon): pin Gemini picker fallbackModels order

mrcfps flagged on PR #986 that the new fallback ids only had implicit
coverage via the static array, which means a future reorder/removal in
AGENT_DEFS could silently reshape the Settings UI without tripping CI.
Mirror the codex/qoder regression-test pattern so the Gemini picker
contents stay locked to the documented priority order: default,
gemini-3-pro-preview, gemini-3-flash-preview, gemini-2.5-pro,
gemini-2.5-flash, gemini-2.5-flash-lite.

This is especially worth covering for Gemini because the CLI accepts
arbitrary custom ids, so a regression in the picker would not surface
during manual QA.

---------

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
2026-05-09 08:15:34 +08:00
Nagendhra Madishetti
72fd9a73a2
fix(web): keep chat auto-scroll glued to bottom across streaming chunks (#989)
Issue #983: when an assistant turn streamed in, the chat log stopped
auto-scrolling even though the user was sitting at the bottom of the
conversation. Root cause: the auto-scroll effect re-measured
`scrollHeight - scrollTop - clientHeight` AFTER the new chunk had
already grown the element. A single tool-use card or markdown render
adds 100+ px in one tick, so the post-content distance check (`< 80`)
skipped the scroll exactly when the user expected it most.

Switch the gate to the existing `scrolledFromBottom` state. That flag
is maintained by the user-driven scroll listener (only flipped by a
real scroll event, not by content growth), so it carries the user's
pre-content intent through to the effect. New content auto-scrolls
when the user was glued to the bottom; scrollback sessions still
preserve their position.

Existing chat-scroll-preservation tests still pass (6/6); the prior-
state behavior we test there is bottom-pinned vs absolute-restore on
tab switch, which this change does not affect.

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
2026-05-09 08:15:15 +08:00
Nagendhra Madishetti
34d781b364
fix(web): differentiate selected, hover, and focus states in the language switcher (#987)
Issue #628: hover and selected rows in Settings → Language used the same
--bg-subtle background, so the currently-selected language was hard to
distinguish from a row being hovered, and keyboard focus had no separate
treatment at all.

- Selected row gets --accent-tint background + accent text + medium
  weight so it reads as the active choice even when nothing is hovered.
- Selected + hover lifts to --accent-soft so the cursor still gives
  feedback over the active row.
- :focus-visible adds a 2px accent outline (inset) so keyboard users
  see exactly where they are while scrolling the list.

CSS-only change; the existing .active class wiring in the React component
already drives this.

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
2026-05-09 08:14:18 +08:00
Morzorz
83ddf7609c
fix(web): correct srcdoc injection and deck bridge for JS strings con… (#938)
* fix(web): correct srcdoc injection and deck bridge for JS strings containing HTML tags

- Use indexOf for </head> (real tag precedes JS string occurrences)
- Use lastIndexOf for </body> (real tag follows JS string occurrences)
- Scope deck bridge slide selector to direct children of deck containers
  to avoid counting cloned overview thumbnails
- Update .slide-number data attributes and .progress-bar width from
  deck bridge's updateDeckChrome so page counter and progress track
  with deck navigation

* fix(web): move deck chrome sync into report() for all slide modes

.slide-number and .progress-bar updates were only in updateDeckChrome()
which is called exclusively from setActive(). Scroll decks go through
scrollGo() → report() without ever calling setActive(), so their page
counter and progress bar stayed frozen.

Move the sync logic into report() which is the convergence point for
all navigation paths (class-toggle, scroll, keyboard-dispatch).

* fix(web): use DOMParser for srcdoc injection instead of string matching

Replace all regex/indexOf/lastIndexOf based </head> and </body> matching
with DOM-based injection via domMutate helper. DOMParser correctly
separates raw-text element content from structural markup, so </head>/
</body> inside JavaScript strings no longer hijack injection points.

Also: scope deck bridge slide selector to direct children of deck
containers, and move .slide-number/.progress-bar sync into report()
so all navigation paths update page chrome.

* fix(web): add robust string fallback for srcdoc injection in non-browser environments

When DOMParser is unavailable (Node tests), fall back to structural
boundary matching: lastIndexOf('</head>', before <body) and
lastIndexOf('</body>', before </html>) to skip literal occurrences
inside script/style blocks. Annotate catch blocks.

---------

Co-authored-by: yangjingting <yangjingting@yxqiche.com>
2026-05-09 02:41:49 +08:00
Sid
362b92c1a6
fix(packaged): swallow harmless setTypeOfService EINVAL from undici (#895) (#906)
* fix(packaged): swallow harmless setTypeOfService EINVAL from undici (#895)

Opening Settings → Pets → Community in the packaged desktop app
surfaced a native "JavaScript error in main process" dialog with
`Uncaught Exception: Error: setTypeOfService EINVAL`. Root cause:
undici's socket setup tries to set the IP_TOS byte for QoS / DSCP
marking on outbound sockets, and the macOS kernel refuses with
EINVAL on certain configurations (VPNs, IPv6-only sockets, some
firewall postures). The byte is purely advisory — the socket
itself is healthy and serves traffic — so the rejection should
not crash the app.

Two cooperating layers:

1. **`protocol.ts`** registers the `od://` scheme that backs every
   renderer page load and API call in the packaged build by
   forwarding through Node's global `fetch` (which is undici under
   the hood). Pulled the inner request handler out as
   `handleOdRequest()` so a test can drive it with a stub fetch,
   and wrapped the `await fetch()` in a try/catch that returns a
   502 Response on failure. Without this, every undici rejection —
   not just `setTypeOfService` — propagated to Electron's default
   uncaught-exception path. Now the renderer sees a normal error
   response and the main process keeps running.

2. **`logging.ts`** adds a defensive `process.on('uncaughtException')`
   handler with a narrow filter, `isHarmlessSocketOptionError`,
   that only matches the canonical undici shape (message contains
   `setTypeOfService` AND code is `EINVAL` or message contains
   `EINVAL`). For any unrecognised error the handler re-throws
   via `setImmediate` so Node's default crash + Electron's
   crash dialog still fire end-to-end — a future regression that
   broadens the filter to "every EINVAL" is caught by the unit
   tests below.

Tests: 13 new tests across `tests/protocol.test.ts` (5) and
`tests/logging.test.ts` (8) pin both layers — including the
explicit #895 regression case (fetch rejecting with the canonical
EINVAL shape returns a 502 instead of throwing) and the negative
guard against the filter swallowing real bugs (a generic write
EINVAL or a setTypeOfService EACCES is *not* matched).

Verified locally:
- `pnpm --filter @open-design/packaged vitest tests/protocol.test.ts tests/logging.test.ts` → 13/13
- packaged `tsconfig.json` and `tsconfig.tests.json` (the CI killer): both clean
- the one pre-existing failure in `tests/sidecars.test.ts` (`adds custom VP_HOME/bin …`) is independent of this PR — confirmed by stashing the change and re-running

* fix(packaged): break recursive rethrow + tighten EINVAL filter (#906 review)

@mrcfps and @lefarcen both flagged a real P1 in the first iteration:
the non-harmless branch of the new uncaughtException handler
rethrew via setImmediate while the same listener was still
registered, so a real bug would re-enter the handler indefinitely
instead of terminating. mrcfps reproduced the loop with a minimal
Node script. lefarcen also flagged that the filter trusted the
message string over a contradicting structured `code`.

Both fixes:

1. **Recursive rethrow (P1).** Extract the handler as a named
   factory, `createFatalUncaughtExceptionHandler(logger)`, that
   captures itself in closure. On non-harmless errors the handler
   now `process.removeListener('uncaughtException', self)` before
   scheduling the rethrow. With no listener registered, the next
   throw lands in Node's default crash path — which is exactly
   what we wanted ("preserve fail-fast for real bugs").

2. **`code` is authoritative (P2).** When `code` is present on the
   error, only `code === 'EINVAL'` qualifies. A contradicting
   `EACCES`/`EPERM` paired with `setTypeOfService EINVAL` in the
   message now slips through to the crash path instead of being
   swallowed. Message-based detection only fires when `code` is
   genuinely absent (some libuv builds don't populate it on raw
   thrown Errors).

3 new tests pin both fixes:
   - `does NOT match when code contradicts the message` and the
     EPERM variant guard against the P2 regression.
   - `removes itself from uncaughtException listeners before
     scheduling the rethrow` uses `vi.spyOn(process,
     'removeListener')` and a stubbed setImmediate to assert the
     call order: removeListener fires before setImmediate
     schedules the throw.
   - `does NOT re-enter itself when invoked twice` is a
     belt-and-suspenders loop guard — even if a future refactor
     dropped the removeListener call, the test would catch
     runaway scheduling.

Verified locally:
- packaged vitest: 18/18 (was 13, +3 new tests; +2 negative-guard
  tests for the P2 filter; -0 deletions)
- packaged tsc -p tsconfig.json --noEmit: clean
- packaged tsc -p tsconfig.tests.json --noEmit (the CI killer): clean
2026-05-09 01:16:23 +08:00
Jie Zhu
32c36f44a7
fix(i18n): rename live artifact tab label in zh-CN and zh-TW (#969)
* fix(i18n): rename live artifact tab label in zh-CN and zh-TW

Remove redundant '新建/新建' prefix from live artifact tab and title
labels to match other tab naming convention (e.g., '原型', '幻灯片').

- zh-CN: '新建实时制品' → '实时制品'
- zh-TW: '新建即時成品' → '即時成品'

* fix(i18n): revert titleLiveArtifact to original value

Only tab label should be renamed; title should keep '新建' prefix
to match other title labels (新建原型, 新建幻灯片, etc.).
2026-05-09 01:15:14 +08:00
Nicholas-Xiong
f50c34fcd4
fix: ensure Settings close button is always clickable (#971)
- Increase .settings-chrome z-index from 3 to 10 to ensure it stays above all modal content
- Remove pointer-events: none from .settings-chrome container to avoid click interception issues
- .settings-autosave already has pointer-events: none, so it won't block clicks

The close button was becoming unclickable after PR #681 introduced the new
Settings layout with Orbit summaries. The issue was likely caused by either
z-index stacking or the pointer-events pattern not working reliably in all
scenarios.

Fixes #905
2026-05-09 00:16:26 +08:00
pmasadali20776-ui
b578b93f3f
Bug FIx: Media generation task state is volatile and lost on daemon restart #648 (#884)
* feat: implement media tasks persistence

* fix(daemon): satisfy exactOptionalPropertyTypes in media-tasks-routes test

`process.env.OD_DATA_DIR` is `string | undefined`, but `openDatabase`'s
options accept `{ dataDir?: string }`. Under the daemon tsconfig's
exactOptionalPropertyTypes the two are not assignable. Spread the key
in only when defined.

* fix(daemon): restore mcp-config / mcp-oauth / mcp-tokens imports in server.ts

The earlier 'Merge branch main into main' resolved the import-block
conflict by keeping only the new media-tasks import and dropping the
three pre-existing import blocks. server.ts still uses 13+ symbols
from those modules (PendingAuthCache, MCP_TEMPLATES, beginAuth,
readMcpConfig, getToken, etc.), so the daemon crashed at startup with
'ReferenceError: PendingAuthCache is not defined' the moment Playwright
booted it. Restore the three import blocks verbatim from main.

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-05-09 00:00:18 +08:00
Sid
05461c64fd
fix(connectors): show stable curated tool count in connector card badge (#748) (#767)
* fix(connectors): show stable curated tool count in connector card badge (#748)

The connector card's "N tools" badge in `apps/web/src/components/
EntryView.tsx` rendered `connector.tools.length` for both pre- and
post-Composio-hydration states, so the displayed count lurched
without explanation:

- Before configuring a Composio API key, GitHub showed "2 tools"
  (the static fallback catalog at `composio.ts:21-53`).
- After hydration, the daemon merged in the full Composio provider
  inventory (`composio.ts:725-778`, ~868 GitHub tools), and the
  same badge jumped to "868 tools".

Same field name, two different concepts — `connector.tools` is
"everything the provider ships" while the agent-callable subset
is `definition.allowedToolNames` (read-only auto-approval tools
that pass `isAgentPreviewListableTool()`).

Fix: surface `allowedToolNames` on the wire `ConnectorDetail` and
have the badge use that count instead. The detail drawer below
the card still iterates over `connector.tools` to enumerate the
full inventory — the count and the list are intentionally
different surfaces.

The badge now stays close to "tools the agent can actually
invoke" (≈2-30 for GitHub, depending on auto-allowed read tools)
instead of the raw provider inventory (~868). No 800x jump on
hydration.

Wire format change:
- packages/contracts/src/api/connectors.ts: add
  `allowedToolNames: string[]` to ConnectorDetail
- apps/daemon/src/connectors/catalog.ts: same field on the
  daemon-internal type, populated in `connectorDefinitionToDetail()`
  as a defensive copy of `definition.allowedToolNames`
- packages/contracts/src/examples.ts: extend the example fixture
- apps/web/src/components/EntryView.tsx: badge call sites switch
  to `connector.allowedToolNames?.length ?? connector.tools.length`
  (the `??` keeps the badge alive against any older daemon build
  that hasn't shipped the field yet)

Tests:
- 4 new daemon tests in connectors-service.test.ts pin the
  contract: getConnector() emits the field, the array is a
  defensive copy, and the #748 regression guard simulates the
  Composio post-hydration shape (tools.length=801,
  allowedToolNames.length=1) to prove the badge invariant
- web EntryView.test.ts fixtures updated to satisfy the new
  required field

Verified locally:
- daemon vitest: 925/925
- web vitest: 332/332
- daemon/web/contracts typecheck clean
- i18n-check passes
- Live `/api/connectors/discovery` returns the new field; pre-
  hydration GitHub/Notion/Google Drive badges all read "2 tools"
  (no regression vs before this change)

Fixes #748

* fix(connectors): split drawer badge vs inventory counts; fix daemon test typecheck (#767 review)

Two follow-ups for @mrcfps's review on PR #767.

1) **P1: Drawer empty-state regression.** The previous commit reused
the curated `toolCount` for both the header badge AND the inventory
section's loading gate / empty-state branch / section count. The
inventory section renders `connector.tools` directly, so a hydrated
connector with raw provider tools but an empty allowlist (e.g. a
write-only Composio surface) would render "no tools available" and
hide the actual inventory list — exactly the contradiction my own
PR description warned against.

Fix splits the two surfaces:
  - `badgeToolCount` (curated, via the new exported helper
    `getConnectorBadgeToolCount`) feeds the card and drawer header
    badge — the summary count, where the #748 stability matters.
  - `inventoryToolCount = connector.tools.length` (inline) drives the
    drawer's loading gate, section count, and empty-state branch —
    the surfaces describing the actual rendered list.

The card has no inventory section so it stays on the badge helper
unchanged.

2) **CI: daemon test typecheck failed.** The connectors-service test's
`provisionedTools[0].safety` index access tripped daemon
`tsconfig.tests.json`'s `noUncheckedIndexedAccess` strict setting,
even though my local `tsc -p tsconfig.json --noEmit` was clean —
that config is a separate compilation. Bind through a defined-checked
local before reading `.safety`, per @mrcfps's exact suggestion.

Tests:
  - 4 new web tests in `EntryView.test.ts` pin the
    `getConnectorBadgeToolCount` contract, including the explicit
    regression: a connector with `allowedToolNames=[]` and
    `tools.length=800` returns badge=0 but the inventory length
    stays at 800 — the drawer's empty-state branch must use the
    inventory count, never the badge count.
  - Existing daemon test fixed without losing assertion coverage.

Verified locally:
  - daemon vitest: 921/921
  - web vitest: 336/336 (was 332, +4)
  - daemon `tsc -p tsconfig.json` and `tsc -p tsconfig.tests.json` (the CI killer): both clean
  - web `tsc -b --noEmit` clean
  - i18n-check passes

Process learning baked into this PR: from now on I'll always run the
`tsconfig.tests.json` separately before pushing, since the workspace
typecheck script chains both and the second one is what CI fails on.

* fix(connectors): pin badge to curated catalog count, not the dynamic execution allowlist (#767 review v2)

@lefarcen and @mrcfps both flagged that the previous iteration of
this PR (commit 447a270) used `allowedToolNames` as the badge
source. That field isn't actually stable across Composio hydration:
`apps/daemon/src/connectors/composio.ts:758-761` extends it with
every provider-discovered tool whose classified safety is
read+auto, so a connector like GitHub goes from 2 → ≈52 on
hydration — better than 2 → 868, but still a 26x jump and still
the kind of unexplained number-lurch the issue is about. My
regression test only covered write-classified tools so this
read-path inflation slipped through.

Fix: introduce `curatedToolNames` as a separate field that is
locked to the static catalog and never extended by discovery.

- `packages/contracts/src/api/connectors.ts` and
  `apps/daemon/src/connectors/catalog.ts`: add
  `curatedToolNames: string[]` to `ConnectorDetail` (required on
  the wire) and as an optional field on
  `ConnectorCatalogDefinition` (defaults to allowedToolNames at
  serialize time).
- `apps/daemon/src/connectors/composio.ts`:
  `definitionFromToolkit()` now sets
  `curatedToolNames = [...staticDefinition.allowedToolNames]` —
  the catalog set, no extension. The runtime `allowedToolNames`
  keeps its dynamic auto-allow behavior so the execution gate is
  unchanged end-to-end.
- `apps/daemon/src/connectors/catalog.ts`:
  `connectorDefinitionToDetail()` populates
  `curatedToolNames: [...(definition.curatedToolNames ?? definition.allowedToolNames)]`,
  so non-Composio connectors (no discovery layer) trivially mirror
  the two fields.
- `apps/web/src/components/EntryView.tsx`:
  `getConnectorBadgeToolCount` becomes a 3-tier fallback —
  `curatedToolNames` first, then `allowedToolNames`, then
  `tools.length`. The order means a half-deployed daemon (allowed
  but not curated) still produces a more meaningful number than
  the raw inventory; only when both are missing do we fall back
  to the original buggy behavior.

Tests added:
- daemon `connectors-service.test.ts`: 3 new tests pin the
  contract — wire shape, hydration stability (catalog stays at 1
  while allowedToolNames grows to 51), and defensive copy.
- web `EntryView.test.ts`: 5 tests on `getConnectorBadgeToolCount`
  including the @lefarcen scenario explicitly: tools=800,
  allowedToolNames=52, curatedToolNames=2 → badge=2.

Verified locally:
- daemon vitest: 924/924 (was 921, +3)
- web vitest: 337/337 (was 332, +5 net)
- daemon `tsconfig.json` and `tsconfig.tests.json` (the CI
  killer): both clean
- web `tsc -b --noEmit` clean
- contracts typecheck clean
- i18n-check passes
- Live wire format: `/api/connectors/discovery` ships
  `curatedToolNames: ['github.github_search_repositories', 'github.github_get_issue']`
  for GitHub (length 2)

* fix: make ConnectorDetail.{allowed,curated}ToolNames optional in shape

After merging main, several existing test fixtures (EntryView,
SettingsDialog.orbit, ConnectorsBrowser) declared `ConnectorDetail`
inline without the new `allowedToolNames` / `curatedToolNames` keys
the PR introduced. Daemon-built payloads always populate both fields
(see `connectorDefinitionToDetail` and the Composio normalization
path), so making the wire shape `?`-optional is safe for runtime
consumers and avoids touching every fixture. Updates the daemon-side
mirror type to match and adds non-null assertions in the daemon tests
that read these fields directly.

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-05-08 23:42:52 +08:00
tenderpooh
109722de3a
feat(desktop): export artifacts directly to PDF (#532)
* feat(desktop): export artifacts directly to PDF

* fix(desktop): PDF 내보내기 기본 여백 제거
2026-05-08 23:42:12 +08:00
Muhammad Ali
0b757c2452
feat(skills): add clinical-case-report skill (#581)
* feat(skills): add clinical-case-report skill

Adds a new healthcare skill for generating structured medical case
presentations (SOAP format, conference, and ward rounds).

Files added:
- SKILL.md — od: frontmatter + full agent workflow instructions
- references/checklist.md — P0/P1/P2 medical accuracy validation
- references/case-formats.md — SOAP, conference, and rounds formats
- examples/example-stemi.html — inferior STEMI with cardiogenic shock

Fills the healthcare vertical gap in the current skill catalog.
Includes physiologically consistent vitals, labs, and an
evidence-based management plan using real clinical guidelines.

* fix: address review feedback from lefarcen and mrcfps

- Add prescribing safety gate (Step 5) — warns about missing allergy,
  renal, weight, and pregnancy context before drug recommendations
- Soften physiologic rules from 'must follow' to 'typical patterns' —
  acknowledges afebrile pneumonia, beta-blocker-blunted shock, etc.
- Preserve user-provided values even if atypical for the diagnosis
- Remove incorrect TIMI 0-7 score (UA/NSTEMI scale) from STEMI example;
  retain Killip Class III and Shock Index 1.27
- Fix troponin units: hs-troponin reported as 2400 ng/L (ref <40 ng/L)
  instead of conventional 2.4 ng/mL
- Add table accessibility: <caption> and scope='col' on vital signs
  and laboratory results tables
- Expand PHI checklist item to cover indirect identifiers (MRNs, dates,
  locations, rare conditions, occupation, verbatim stories)
- Disambiguate format selection guide ('ward round' maps to Brief Rounds,
  'formal rounds' maps to SOAP)
- Add example.html at skill root for /api/skills/:id/example resolver

* i18n: add clinical-case-report to DE/FR/RU skill fallback lists

* fix: soften checklist P0 vital signs rule to allow clinical variability

* fix: add medication safety checks block before antiplatelet section in examples

* fix: correct eGFR/age in safety block, add prescribing-safety P0 checklist items

* fix: correct age 67 to 58 in pregnancy line of safety block

* fix: defer norepinephrine dose to local protocol until weight confirmed

* fix: wire reference files into workflow; defer beta-blocker until shock resolved

* fix: close html code fence before Step 7 so checklist gate renders as prose

* fix: restrict oxygen to hypoxaemia only; generalise social history for de-identification

* fix: format-conditional P0 HPI gates; Killip III->IV for cardiogenic shock; smoking status consistent

* fix: make Step 2 and 'What you will produce' format-conditional for Brief Rounds

* fix: remove occupation detail from social history to comply with P0 de-identification rule

* fix: add 'ward rounds' plural to Brief Rounds format-selection table

* fix: gate Step 1 clarification on format; accept Killip+Shock Index as ACS risk scores
2026-05-08 22:36:37 +08:00
Nicholas-Xiong
727936ecb7
fix: remove redundant Connect GitHub placeholder from import menu (#964)
GitHub connection is already available through the Connectors tab.
The disabled 'Connect GitHub' item in the chat composer import menu
was a redundant placeholder that created confusion about duplicate
entry points.

This removes the placeholder to streamline the UI and direct users
to the primary GitHub connection flow via Connectors.

Fixes #777
2026-05-08 22:32:48 +08:00
soulme
e3423c2b7b
feat: add draggable file tab reordering (#936) 2026-05-08 22:21:19 +08:00
Sid
0701dee9b9
fix(desktop): allow od:// URLs in setWindowOpenHandler so live artifact previews open in child window (#911) (#933)
The Orbit panel's "Open artifact" button is an
`<a target="_blank" href="/api/live-artifacts/.../preview?projectId=...">`.
In packaged Electron builds the renderer is loaded from
`od://app/`, so the relative `href` resolves to
`od://app/api/live-artifacts/.../preview?projectId=...` by the
time `setWindowOpenHandler` sees it.

The existing handler in `apps/desktop/src/main/runtime.ts:300`
runs three checks in order:

  1. `isAllowedChildWindowUrl(url)` → only matched `blob:`
  2. `isHttpUrl(url)` → only matched `http:` / `https:`
  3. fall through → `{ action: "deny" }`

The packaged `od://` scheme matched neither, so the click was
silently dropped — Orbit's "Open artifact" became a no-op for
every desktop user. Dev mode (`http://127.0.0.1:port/`) was
unaffected: its links resolved through the http branch and
opened in the user's external browser via `shell.openExternal`.

Fix: extend `isAllowedChildWindowUrl` to also accept the
packaged `od:` scheme. Electron then opens a child BrowserWindow
that inherits the same protocol registration + preload, so the
live artifact preview HTML (served via the `od://` proxy in
`apps/packaged/src/protocol.ts`) renders in the new window.

Behavior delta:

  - Packaged: click → `od://app/api/.../preview` → child window
    renders the live artifact preview (was: silent no-op).
  - Dev mode: unchanged — link is `http://127.0.0.1:port/...`,
    still routes to `shell.openExternal`.
  - Other links inside the packaged app that still flow to
    `shell.openExternal` (e.g. github.com / external docs):
    unchanged — `od:` is not a wildcard, only the packaged
    scheme passes the allowlist.

Tests: exposed `isHttpUrl` and `isAllowedChildWindowUrl` from
`apps/desktop/src/main/index.ts` (re-exported through
`@open-design/desktop/main`) so the packaged workspace's vitest
can pin both helpers without bringing up Electron. 8 tests
across the negative-guard surface — `od:` allowed, `blob:` still
allowed, `http(s):` NOT allowed by this branch (handled by the
sibling `isHttpUrl` path), `file:`, `javascript:`, `data:`
explicitly NOT allowed.

The pre-existing `apps/packaged/tests/sidecars.test.ts >
adds custom VP_HOME/bin to the packaged PATH builder` failure
is unrelated to this PR — confirmed by stashing the change
and re-running on the bare branch base.

Verified locally:
  - packaged vitest desktop-url-allowlist: 8/8
  - desktop tsc -p tsconfig.json --noEmit: clean
  - packaged tsc -p tsconfig.json --noEmit: clean
  - packaged tsc -p tsconfig.tests.json --noEmit (the CI killer): clean
2026-05-08 22:16:35 +08:00
Nagendhra Madishetti
5d7568ba2c
fix(web): confirm before clearing a saved Media provider API key (#875)
The Clear button on Settings → Media providers wiped the saved
apiKey + baseUrl + model in a single click with no recovery — a
fat-fingered click on the wrong row would silently delete a key
the user just pasted in.

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).
The prompt is localized via a new `settings.mediaProviderClearConfirm`
key with `{name}` placeholders for the provider label, translated
across all 17 locales.

Updated the existing media-provider clear test to auto-accept the
prompt, plus added a sibling test asserting that dismissing the
prompt leaves the saved config intact.

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
2026-05-08 22:15:59 +08:00
PerishFire
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
2026-05-08 21:48:54 +08:00
leprincep35700
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>
2026-05-08 21:41:52 +08:00
leprincep35700
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>
2026-05-08 21:38:13 +08:00
leprincep35700
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>
2026-05-08 21:37:49 +08:00
kami
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
2026-05-08 21:20:48 +08:00
Priyanshu Kayarkar
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
2026-05-08 21:19:53 +08:00
leprincep35700
c2facb0e02
fix: serve python files as text (#947)
Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com>
2026-05-08 21:10:22 +08:00
Siri-Ray
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
2026-05-08 21:05:22 +08:00
nettee
ef9ca7baff
fix(daemon): typecheck core server paths (#952) 2026-05-08 20:43:51 +08:00
nettee
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)
2026-05-08 20:01:25 +08:00
Caprika
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
2026-05-08 20:01:06 +08:00
Bryan A
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>
2026-05-08 19:52:11 +08:00
ferasbusiness666
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>
2026-05-08 19:46:34 +08:00
Tom Huang
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>
2026-05-08 17:59:20 +08:00
ashleyashli
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>
2026-05-08 17:49:57 +08:00
Tom Huang
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>
2026-05-08 17:38:29 +08:00
Joey-nexu
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>
2026-05-08 17:21:12 +08:00
Sid
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
2026-05-08 16:50:59 +08:00
Marc Chan
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
2026-05-08 16:48:10 +08:00
shangxinyu1
8fee22d358
Fix stuck chat runs and unintended cancels (#896)
* Fix stuck chat runs and unintended cancels

* Harden chat run stall watchdog
2026-05-08 15:47:44 +08:00
Et cetera
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.
2026-05-08 15:14:37 +08:00
Marc Chan
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>
2026-05-08 14:27:46 +08:00
shangxinyu1
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
2026-05-08 14:26:10 +08:00
lefarcen
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.
2026-05-08 14:16:12 +08:00
Muhammad Anas
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>
2026-05-08 13:05:55 +08:00
Sohaib Kamran
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
2026-05-08 12:49:32 +08:00
Nagendhra Madishetti
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>
2026-05-08 12:39:04 +08:00
Tuola-waj
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>
2026-05-08 12:08:43 +08:00
Tom Huang
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>
2026-05-08 12:02:59 +08:00
VanJay
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>
2026-05-08 11:51:51 +08:00
nettee
8930b9650c
feat: Add a toggle to reveal media provider API keys (#867) 2026-05-08 11:46:21 +08:00
Nagendhra Madishetti
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>
2026-05-08 11:35:23 +08:00
Priyanshu Kayarkar
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
2026-05-08 11:34:53 +08:00
Nagendhra Madishetti
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>
2026-05-08 11:29:37 +08:00
Terence !_!
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>
2026-05-08 11:26:34 +08:00