Commit Graph
531 Commits
Author SHA1 Message Date
shangxinyu1andGitHub f650a043d9 test(e2e): align entry coverage with redesigned flows (#2101)
* Migrate entry E2E coverage and split CI

* test(e2e): relax connectors auth error assertions

* ci: route scenario registry changes to extended e2e

* ci: decouple packaged smoke jobs from validate gate

* ci: restore pre-split workflow

* test(e2e): slim critical ui smoke coverage

* test(e2e): move broader entry flows out of critical

* test(e2e): restore entry chrome coverage to ci

* ci: parallelize workspace validation jobs

* test(web): stabilize media palette bridge assertion

* ci: cache e2e playwright browsers
2026-05-19 11:26:40 +08:00
Joey-nexuandGitHub 56988e406c feat: integrate xAI SuperGrok subscription as a credential source for Grok media + X search (#2134)
* feat(daemon): add xAI OAuth client with PKCE + token storage

Wraps mcp-oauth.ts PKCE primitives for xAI's auth.x.ai OAuth server.
xAI doesn't speak MCP and doesn't expose Dynamic Client Registration,
so issuer / endpoints / client_id / scope / loopback :56121 are
hardcoded constants.

Adds xai-tokens.ts for persistent storage, mirroring mcp-tokens.ts:
atomic write + chmod 0600 + per-dataDir in-memory mutex. Simplified
for the single-token case (no per-server-id map).

Reference: NousResearch/hermes-agent hermes_cli/auth.py:93-100.
PoC reuses Hermes client_id (b1a00492-...); replace before stable
release once Open Design has its own.

Tests: 11 + 20, all green. tsc --noEmit clean. pnpm guard clean.

* feat(daemon): expose xAI Grok models in Hermes runtime fallbackModels

Lists grok-4.3, grok-4.20-reasoning, grok-4.20-non-reasoning, and
grok-4.20-multi-agent-0309 as discoverable Hermes fallback models.
A user who has not installed Hermes yet now sees these xAI options
in the model picker, signalling that `hermes auth add xai-oauth`
(SuperGrok subscription) or XAI_API_KEY unlocks Grok in Open Design
without OD itself implementing OAuth-for-chat.

`fetchModels` (which calls `hermes acp` to enumerate the user's
actually-installed providers) is unchanged; this list only kicks
in when probing fails (e.g. Hermes off PATH).

Reference: xAI × Nous Research grok-hermes integration announcement,
2026-05-15. https://x.ai/news/grok-hermes

* feat(media): route Grok Imagine through xAI OAuth credentials

Adds resolveXAIBearer() — a refresh-aware helper on top of the
xai-tokens.json store written by the daemon's OAuth client. Returns
a fresh access_token, transparently refreshing in-place when the
stored token enters the 120 s expiry skew window.

Wires it into media-config.ts so the existing Grok provider gets the
same OAuth-fallback treatment OD already gives the OpenAI provider:
env keys win, then stored Settings keys, then OD-native xAI OAuth,
then a borrowed Hermes-side xai-oauth token from ~/.hermes/auth.json.
SuperGrok subscribers who already authorized Hermes get OD image /
video generation routed through their subscription with zero extra
setup.

Updates the "no xAI API key" error in renderGrokImage / renderGrokVideo
to point at the new OAuth path so users hitting it know they have a
zero-cost option.

Also exposes mediaConfigDir() so credential helpers next to
media-config.json (like xai-tokens.json) reuse the same precedence:
OD_MEDIA_CONFIG_DIR > OD_DATA_DIR > <projectRoot>/.od.

Tests: 7 new xai-credentials cases (refresh on expiry, refresh
failure, missing refresh_token, response without refresh_token) +
8 new media-config Grok OAuth fallback cases (OD-native, Hermes
borrow, OD vs Hermes precedence, env precedence, stored precedence,
unconfigured, expired-without-refresh). All green; tsc / guard clean.

* feat(media): add xAI Grok TTS provider

Registers grok-tts in the speech model catalog and wires up
renderXAITTS to dispatch (provider=grok, surface=audio, kind=speech)
to https://api.x.ai/v1/tts. xAI exposes a dedicated /tts endpoint
that returns raw audio bytes — distinct from OpenAI's /audio/speech
JSON shape — so TTS gets its own renderer rather than reusing
renderOpenAISpeech.

Credentials route through the same OAuth-aware path as Grok image
and video (PR follow-up to media-config.ts), so a SuperGrok
subscriber gets TTS for free once they have authorized once.

Default request body matches the documented minimal shape
(text / voice_id / language); sample_rate / bit_rate / codec are
left unset so the server applies its mp3 / 24 kHz / 128 kbps
defaults. Plumbing for explicit overrides is left for a later PR
once the agent-facing contract grows the corresponding flags.

Tests: 5 cases covering documented body shape, voice / language
override, env-key fallback, server-error surfacing, and the
no-credentials error. All green; tsc / guard clean.

Reference: https://docs.x.ai/developers/model-capabilities/audio/text-to-speech

* feat(daemon, web): expose xAI OAuth flow in Settings UI

Closes the loop on the Grok integration: a SuperGrok subscriber can
now authorize Open Design directly from Settings → Media Providers →
Grok, with no API key and no Hermes install. After authorizing, image,
video, and TTS routes pick up the bearer through the OAuth fallback
chain added in 'route Grok Imagine through xAI OAuth credentials'.

Daemon side
- xai-oauth-server.ts opens a one-shot HTTP listener on
  127.0.0.1:56121 to receive the OAuth callback. The redirect URI is
  hard-locked to that port because the PoC reuses the Hermes-issued
  client_id. Listener self-closes on first matching callback or after
  a 30 min timeout.
- xai-routes.ts wires three endpoints onto the daemon's HTTP app:
    POST /api/xai/oauth/start       — mint state, open listener,
                                       return authorize URL
    GET  /api/xai/auth/status       — has-token / expiry / in-flight
    POST /api/xai/oauth/disconnect  — wipe stored token, stop listener
- server.ts registers xai-routes alongside the existing mcp-routes.

Web side
- XaiOAuthControl.tsx renders a Sign in / Reconnect / Disconnect
  surface mirroring McpOAuthControl, but polls /api/xai/auth/status
  exclusively because the :56121 callback page lives in a separate
  process and can't postMessage back to the OD UI. SettingsDialog
  embeds it inside the Grok provider row.

Tests: 9 listener cases (bind / state mismatch / replay / favicon /
EADDRINUSE / timeout / explicit error param / one-shot consume /
early stop) + 8 route cases (start mints PKCE URL, second start
replaces in-flight listener, status reports listening + connected,
callback ok stores token, callback error skips storage, disconnect
wipes, cross-origin guard rejects all three endpoints). All 17 +
the 74 from prior commits pass; tsc / web typecheck / pnpm guard
clean.

PoC client_id stays Hermes-issued; user-visible strings are
hardcoded English pending an i18n pass before stable.

* fix(daemon, web): xAI OAuth follow-up — paste-back, X search, UX polish

PoC testing surfaced four real-world rough edges in the Sign in flow
that were not obvious before getting an actual SuperGrok subscription
in front of it. None alter the architecture in 'expose xAI OAuth flow
in Settings UI'; they round it off so the path the user actually walks
matches the one the design assumed.

1. Layout. XaiOAuthControl was a grid item inside .media-provider-body
   and got squeezed into the API-key column. Moves it out of the body
   so the row's flex-column layout gives it the full width — matches
   what every other Settings provider OAuth surface gets.

2. Paste-back. xAI's `auth.x.ai` page often shows a "cannot connect to
   your application" fallback that hands the user a code instead of
   redirecting back to 127.0.0.1:56121, even when the loopback listener
   is reachable (browser DOES quietly redirect in the background, but
   the page lies and shows the manual-paste UI anyway). Adds:
     - POST /api/xai/oauth/complete that takes {state, code} and runs
       completeXAIAuth + setXAIToken + stops the listener.
     - A paste-back input row in XaiOAuthControl that surfaces while
       the dance is in flight; submitting either via Enter or the
       button calls /complete and falls through to the same connected
       state the loopback path lands on.

3. X search. New POST /api/xai/search wraps Grok's native x_search tool
   through the Responses API, gated on the same OAuth-first credential
   chain as Grok image / video / TTS. Body accepts query (required),
   allowed_x_handles, excluded_x_handles, from_date, to_date, model.
   Returns { answer, citations[], model } parsed from the Responses
   payload via two newly exported helpers (extractAnswerText,
   extractUrlCitations).

4. State machine + warning banner. Three issues collapsed into one:
     - Polling that flipped busy → 'idle' the moment the loopback
       listener self-closed disabled the paste-back input even though
       the dance was still recoverable. Removed that branch; awaiting
       state now only ends on connected=true or explicit cancel.
     - paste-input `disabled` was over-eager (`busy !== 'awaiting' &&
       busy !== 'refreshing'`); now it's only blocked while a submit
       is in flight (`busy === 'refreshing'`).
     - Added a heads-up banner inside the awaiting region explaining
       that xAI's "cannot connect" page is a UX bug on their side and
       the OD panel is the source of truth for sign-in success. The
       connected message picks up the cue too: "You can close any
       open xAI browser tabs now."

Tests: +12 cases on top of the existing 17. The complete endpoint
covers happy path, blank-field rejection, and unknown-state error.
The search endpoint covers blank-query rejection, no-credentials 401,
full bearer / x_search-options forwarding with response parsing, and
upstream-error pass-through. Two helper functions get four direct
parser cases. All 29 in the file pass; 225 across the daemon test
suite pass; tsc / web tsc / pnpm guard all clean.

* fix(daemon): satisfy tsconfig.tests.json strictness in xai test files

The CI workspace typecheck step runs tsconfig.tests.json (which extends
tsconfig.json's strict + exactOptionalPropertyTypes settings and adds
the tests/ directory to the include set) — but the local
`tsc -p tsconfig.json --noEmit` I ran while iterating only covered
src/. That gap let two classes of strict-mode errors slip into the
PR's CI:

- `let outcome: CallbackOutcome | null = null` mutated from inside an
  async callback narrowed to `never` after `outcome?.kind` because TS
  doesn't track cross-function mutation. Switched the seven sites in
  xai-oauth-server.test.ts to a `{ current: CallbackOutcome | null }`
  ref object — TS does narrow .current correctly, so `kind` / `error`
  field access stops collapsing to `never`.
- `await r.json()` returns `Promise<unknown>` in the lib.dom typings
  shipped with TS 5.x, so every `body.field` / `status.connected`
  access in xai-routes.test.ts tripped TS18046. Added a one-line
  `jsonOf<T = any>` helper at the top of the file and switched all
  call sites (both `await r.json()` and `.then((r) => r.json())`).
- The cross-origin guard test iterated `for (const [method, path] of
  [...])` — under noUncheckedIndexedAccess that destructures to
  `string | undefined`, which RequestInit.method (a `string` under
  exactOptionalPropertyTypes) won't accept. Hoisted the cases to a
  typed `ReadonlyArray<readonly [string, string]>` so the elements
  stay non-optional.

Behaviour is unchanged; vitest still reports 29/29 across these two
files. tsc -p tsconfig.tests.json --noEmit now passes locally,
matching what CI will run.

* fix(xai-oauth): preserve refresh_token + release :56121 on cancel

Two lifecycle issues Looper flagged on the prior commit:

1. resolveXAIBearer dropped the existing refresh_token whenever the
   refresh response omitted one. RFC 6749 §6 explicitly allows the
   server to skip refresh_token rotation and keep the old one valid;
   xAI's behaviour is currently to rotate, but a future change could
   silently break OD users. With the old code the first refresh
   succeeded but persisted a token with no refresh credential, so the
   next expiry forced the user back through Sign in even though their
   grant was still good. Carries the previous refresh_token forward
   when fresh.refresh_token is absent. Updates the matching
   xai-credentials test to assert the carried-forward value instead of
   the previous (incorrect) "drop it" assertion.

2. The Cancel button in XaiOAuthControl only cleared React-side
   pending state; the daemon's one-shot 127.0.0.1:56121 listener kept
   running for the full 30 min server timeout. /api/xai/auth/status
   would still report listening=true, and that singleton port could
   block the next Sign in (or a Hermes session on the same machine).
   Adds POST /api/xai/oauth/cancel that calls stopActiveListener()
   without touching the stored token (Disconnect is the destructive
   path; this is the narrow "release the port" affordance), wires the
   UI Cancel handler to fire it, and adds two route tests covering
   the listener-stopped-but-token-preserved invariant and the no-op
   behaviour when no listener is in flight.

All 38 xai tests + tsconfig.tests.json typecheck + web typecheck +
pnpm guard pass.

* fix(xai-oauth): close two more lifecycle gaps Looper flagged

Both are non-blocking but cheap and right.

1. window.open used 'noopener=no,noreferrer=no' (carried over from the
   sibling McpOAuthControl), which deliberately KEEPS the auth.x.ai
   tab's window.opener reference back to the Settings tab. Reverse
   tabnabbing risk if the auth page or any redirect target along the
   OAuth chain ever turns hostile, with no upside — the xAI flow
   doesn't use postMessage, the daemon receives the code through the
   :56121 listener (or paste-back), so opener access buys nothing.
   Switched to 'noopener,noreferrer'.

2. PendingAuthCache was constructed with its default 10 min TTL while
   the loopback listener self-closes at 30 min and the UI shows a
   pending state for the same 30 min. After 10 min, a user looking at
   a live paste-back input would hit `xAI OAuth state not found or
   expired` even though everything visible (and the daemon socket)
   still claimed the dance was live. Constructed the cache with
   30 * 60 * 1000 so the PKCE state, the open :56121 socket, and the
   paste-back UI all expire together.

The third inline comment (XaiOAuthControl.tsx:248 — "Cancel only
clears React-side state") was a stale reference: the previous commit
fd04887 wired the Cancel button to fire `cancelInFlightOAuth()` which
hits the new `POST /api/xai/oauth/cancel` endpoint. Looper carried
the old comment forward when re-reviewing the rebased file; no code
change needed.

All 38 xai tests still green; tsconfig.tests.json clean; web tsc
clean; pnpm guard clean.

* fix(xai-oauth): keep loopback listener open on stale-tab callbacks

The one-shot listener marked itself consumed at the top of every
/callback request, then closed itself in the finally block whether
or not the state actually matched. A stray browser tab replaying an
old /callback?state=… (real-world scenario: user re-clicked Sign in
before closing the previous tab) would therefore close the singleton
:56121 listener with a state-mismatch error before the real xAI
redirect could arrive.

Now we only tear the listener down on outcomes that actually
terminate the dance:
  - ok callback (matched state, code present)
  - explicit ?error= from xAI (auth provider terminated; we should
    propagate, not wait for the 30 min timeout). xAI's error
    redirects may or may not echo state, but a stale tab can't
    fabricate ?error= without colluding with the auth server, so
    this branch is safe to consume.

Stale tabs / browser prefetches / malformed redirects still get the
HTTP 400 / "Sign-in failed" page, but the listener stays open and
the matching xAI redirect that arrives next is what closes it.

Tests: replaces the previous "rejects state mismatch with kind=error"
test with the recovery scenario (stale-then-real callbacks both hit
the listener; only the real one fires onCallback). Adds a sibling
case for missing-code / missing-state callbacks. xai-oauth-server
suite is now 10/10; full xai sweep 39/39.

* fix(xai-oauth): scope error-callback consume to matching/missing state

c00252c simplified the consume rule to "any explicit ?error= closes
the listener", which was broader than the stale-tab protection added
in the same commit. A browser history replay of an old
`/callback?error=access_denied&state=stale` would set `consumed`,
fire `onCallback`, and tear down the singleton 127.0.0.1:56121 socket
before the current dance's real callback could land — undoing the
defence the commit was supposed to add.

Tighten the rule so error-callbacks consume only when:
  - the URL carries no state (xAI rejected before issuing one, so
    there's nothing to compare against — safe to terminate), or
  - the carried state matches our expectedState (xAI explicitly
    rejected this dance; propagate immediately rather than wait for
    the 30 min timeout).

An ?error= replay carrying a *different* state is now treated like
the stale success replay above: returns the 400 page to the browser,
keeps the listener live, lets the real callback close it.

Tests: adds two cases — error+wrong-state followed by real success
must still resolve to ok; error+matching-state still consumes the
listener and surfaces the error to onCallback. xai-oauth-server
suite goes 10 → 12; full xai sweep 39 → 41.
2026-05-19 11:10:34 +08:00
34f66113a0 Implement Home audio essential workflow (#2104)
* fix(web): align Home prompt overlay with textarea so caret lands on click

Picking a chip such as Slide deck or Image loaded a default prompt
into the Home textarea and rendered an overlay with `{{key}}`
placeholders as interactive `<input>` / `<select>` controls. The
overlay controls and the underlying textarea text were laid out
independently:

- Inputs declared `min-width: 8ch` and `Math.max(displayValue.length
  + 1, 10)ch` of width.
- Selects added 18px of right-padding for the dropdown arrow.
- The textarea kept the raw substituted string in a proportional
  font.

The two layouts no longer matched column-for-column, so every slot
shifted the textarea text to the left of where it appeared in the
overlay, compounding across the line. Clicking on visible prose to
position the caret hit a different character offset in the textarea
and subsequent typing or deletion landed in the wrong place.

For example, the Slide deck template

  Create a {{slideCount}}-slide {{deckType}} for {{audience}} about
  {{topic}}. ...

renders with slideCount=10 (~2 ch in the textarea) under a slot
input forced to 10 ch in the overlay — clicking right after the
literal `-slide` placed the caret several characters into `pitch
deck`. The Image / Video / Audio chips with their pre-filled
subject, style, aspect values reproduced the same drift.

Render the inline pills as read-only `<span>`s carrying the exact
substring the textarea shows at that position, mark them
`aria-hidden` so the textarea remains the single labelled control,
and surface every plugin input field — including the ones referenced
inline — in `PluginInputsForm` underneath. Editing flows through the
form, the parent's `updateActiveInputs` already re-renders the
prompt, and the pills stay aligned with the textarea on every
keystroke.

Also drop the now-unused inline helpers (updatePluginInput,
getTemplateInputNames, shouldRenderSlotAsText, inlineFieldType,
fileInputLabel, fileMetadata) and the dead
`.home-hero__prompt-slot-control/input/select/toggle/file/text` CSS
rules.

Verified:
- pnpm --filter @open-design/web typecheck
- vitest run on HomeHero.plugin-picker, HomeHero.rail, and
  HomeView.prefill (29/29 pass; tests updated to reflect the new
  read-only span + always-on form contract)
- Manual click-to-edit on Slide deck and Image chips in the
  pnpm tools-dev web runtime — caret now lands where the user
  clicked.

* Implement home audio essential workflow

* Fix Home media composer review issues

* Guard stale Home media apply results

---------

Co-authored-by: hahaplus <zmjdll@gmail.com>
2026-05-18 23:14:03 +08:00
Lucky PathanandGitHub 15161f685b fix(web): improve replace modal button hover contrast (#2114) 2026-05-18 22:13:09 +08:00
1523defad1 feat(web): add plugin registry detail drawer (#2087)
Co-authored-by: multica-agent <github@multica.ai>
2026-05-18 22:12:16 +08:00
kamiandGitHub 8a629eb999 fix: discover codex models from cli (#2082) 2026-05-18 22:11:51 +08:00
Nicholas-XiongandGitHub b8c2f9eb82 fix: localize Projects page title (#2077) (#2118)
- Replace hardcoded 'Projects' with t('entry.navProjects')
- Uses existing translation key (already defined in en.ts and zh-CN.ts)
- Page title now displays as '项目' in Chinese
2026-05-18 20:27:48 +08:00
f7eb82d7a5 feat(design-systems): import design system projects (#2112)
* feat(design-systems): define project manifest contract

* feat(design-systems): add default project manifest

* feat(daemon): consume design system manifests

* feat(design-systems): import local project systems

* feat(design-systems): import from github repositories

---------

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
2026-05-18 20:20:38 +08:00
kamiandGitHub ec53959601 feat(web): unify plugin trust badges (#2088)
Normalizes plugin trust badge rendering across Home, plugin details, registry entries, sources, and plugin share/install surfaces while mapping bundled and official sources to the user-facing Official tier.

Validation: CI and nix-check were green on PR head 1a68f20571.
2026-05-18 19:20:01 +08:00
kamiandGitHub ac748d53fc Fix pointer HTML artifact previews (#2075)
Resolves short pointer-only HTML artifacts to the existing project HTML target before persisting preview artifacts, with helper and ProjectView regression coverage for #536. Validation: CI and nix-check were green on PR head 8c7ee5f38d.
2026-05-18 18:28:28 +08:00
Siri-RayandGitHub 6596e14ac2 Fix home entry actions (#2038)
Routes the Home quick-start actions to the intended folder-import and template-entry flows, with shared pick-and-import error formatting and restored macOS tab-strip drag behavior. Validation: CI and nix-check were green on PR head ef743e6a01.
2026-05-18 18:05:59 +08:00
kamiandGitHub 7b3b7c3b74 Fix finalize provider routing for Gemini BYOK (#1964)
Routes Finish Design/finalize requests through the selected BYOK provider, including Gemini, while preserving the Anthropic fallback. Validation: CI and nix-check were green on PR head 6c334e08d1.
2026-05-18 18:03:44 +08:00
kamiandGitHub 0101a09b10 fix(mcp): support no-auth local HTTP servers (#2008) 2026-05-18 17:08:46 +08:00
25708fbe0f fix(web): keep connector errors out of cards (#1740)
* fix(web): keep connector errors out of cards

* fix(web): route panel-alert open-details through openConnectorDetails

The new "open details" action on the panel-alert row called
`setDetailConnectorId(alert.connectorId)` directly, bypassing the
`toolPreviewFailedIds` clear that `openConnectorDetails()` does on
every other entry point.

Concrete regression: user opens a connector's drawer from the card,
the tool-preview hydration fetch fails so `toolPreviewFailedIds[id]`
gets stamped with the current `toolPreviewRetryToken`, user closes
the drawer, and later the same connector throws a connect error and
shows up as a panel alert. Clicking the alert's open-details button
re-sets `detailConnectorId` but never clears the failed-fetch token,
so the hydration effect at the previous bail-out check
(`toolPreviewFailedIds[detailConnector.id] === toolPreviewRetryToken`)
returns immediately and the drawer never retries loading tools.

Routing the click through `openConnectorDetails(alert.connectorId)`
restores the same clear-before-set behavior the card and search
entry points already had, so the reopened drawer always gets a fresh
hydration attempt.

* test(web): cover connector panel alert tool retry

* fix(web): clarify connector panel alert status text

* fix(web): clear stale connector cancel alerts

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-05-18 17:06:46 +08:00
David ZandGitHub 2a592cd37c Refresh and polish French UI locale (#1691)
* Refresh French UI locale coverage

* Address French locale review feedback

* Sync French locale with latest main

* Update French locale after latest main
2026-05-18 17:06:12 +08:00
Ethan GuoandGitHub bd1b41a9d4 feat(web): highlight captured Pod component on chip hover (#1982)
* feat(web): highlight captured Pod component on chip hover

Wires onPointerEnter/Leave on chip + onFocus/Blur on its × so the matching member overlay gets an outline-focused emphasis on canvas.

* ci: trigger re-run for flaky palette dark-mode test

* fix(web): skip non-mouse pointer events on pod chip hover

Gate onPointerEnter/Leave to pointerType === 'mouse' so a touch tap on a chip no longer flickers the captured-member highlight; also add the CommentTargetOverlay class-wiring test, drop the redundant alpha on the hover outline, and document the non-member fallback path.
2026-05-18 16:34:19 +08:00
92d91101ae Fix inspect ancestor selection notice (#2039)
Co-authored-by: fuyizheng3120 <182291973+fuyizheng3120@users.noreply.github.com>
2026-05-18 16:33:42 +08:00
Jeongjin ShinandGitHub 0026dfa344 fix: support CODEX_API_KEY for Codex CLI (#1880)
* fix(daemon): allow Codex API key env

* fix(web): expose Codex API key setting

* fix(web): clarify Codex key labels
2026-05-18 14:29:23 +08:00
Yuhao ChenandGitHub 156191d565 fix(web): show unavailable state for missing examples (#1983) 2026-05-18 14:02:22 +08:00
Abe PenaandGitHub fa9f1d37be feat(design-files): add filter-by-kind dropdown to the design files viewer (#1990)
* feat(design-files): add filter-by-kind dropdown

The design files viewer already supports grouping and sorting by kind,
but on projects with many files of mixed kinds you still see everything
in one list. Adds a small dropdown next to the group-by toggle that
filters the visible files to one or more selected kinds.

Behavior:
- Dropdown only appears when the project has at least two distinct
  kinds; on a single-kind project the control would be noise.
- Multi-select via checkboxes. Empty set means no filter (show all).
- Each row shows the kind glyph, label, and the per-kind file count.
- Trigger label collapses sensibly: "Filter by kind" when none selected,
  the single kind's label when one selected, "{n} kinds" when many.
- Filter applies upstream of sort/group/page, so pagination ranges and
  the "1-N of M" counter reflect the filtered total.
- Page resets to 0 on filter change so the user doesn't land on an
  empty page.
- Selected kinds that disappear from the file list (after a delete or
  refresh) are pruned automatically — a stale filter cannot silently
  hide everything.
- Popover closes on outside click and Escape.

i18n keys: designFiles.filterBy, designFiles.filterClear,
designFiles.filterCount. All non-English locales inherit via the
existing `...en` spread pattern; zh-CN is the one fully-explicit
dictionary and gets translated values inline.

No behavior change to the group-by toggle, sort, pagination, selection,
or any other existing control. Filtering is layered alongside.

* fix(design-files): prune selection when filter hides selected rows

Codex review flagged that the filter-by-kind change only reset
pagination, not selection. If a user selected files, then narrowed the
filter to a kind that excludes some of them, the now-hidden rows
remained in `selected`. Bulk delete / download still operated on the
full set, so the user could destructively act on rows they could no
longer see — especially risky for the delete path.

Add a useEffect that prunes `selected` to the filtered set whenever
`filteredFiles` or `kindFilter` changes. The empty-filter branch is a
no-op so clearing the filter doesn't disturb any pre-existing
selection — only narrowing the filter prunes.
2026-05-18 11:34:13 +08:00
332e982a07 fix(web): stabilize HTML preview URL on tab switch (#1959)
Co-authored-by: GHX5T-SOL <200635707+GHX5T-SOL@users.noreply.github.com>
2026-05-17 23:25:14 +08:00
91be2696dd Persist routine failure reasons (#1963)
Co-authored-by: multica-agent <github@multica.ai>
2026-05-17 23:22:00 +08:00
hahaplusandGitHub 89bf313782 fix(web): align Home prompt overlay with textarea so caret lands on click (#1958)
Picking a chip such as Slide deck or Image loaded a default prompt
into the Home textarea and rendered an overlay with `{{key}}`
placeholders as interactive `<input>` / `<select>` controls. The
overlay controls and the underlying textarea text were laid out
independently:

- Inputs declared `min-width: 8ch` and `Math.max(displayValue.length
  + 1, 10)ch` of width.
- Selects added 18px of right-padding for the dropdown arrow.
- The textarea kept the raw substituted string in a proportional
  font.

The two layouts no longer matched column-for-column, so every slot
shifted the textarea text to the left of where it appeared in the
overlay, compounding across the line. Clicking on visible prose to
position the caret hit a different character offset in the textarea
and subsequent typing or deletion landed in the wrong place.

For example, the Slide deck template

  Create a {{slideCount}}-slide {{deckType}} for {{audience}} about
  {{topic}}. ...

renders with slideCount=10 (~2 ch in the textarea) under a slot
input forced to 10 ch in the overlay — clicking right after the
literal `-slide` placed the caret several characters into `pitch
deck`. The Image / Video / Audio chips with their pre-filled
subject, style, aspect values reproduced the same drift.

Render the inline pills as read-only `<span>`s carrying the exact
substring the textarea shows at that position, mark them
`aria-hidden` so the textarea remains the single labelled control,
and surface every plugin input field — including the ones referenced
inline — in `PluginInputsForm` underneath. Editing flows through the
form, the parent's `updateActiveInputs` already re-renders the
prompt, and the pills stay aligned with the textarea on every
keystroke.

Also drop the now-unused inline helpers (updatePluginInput,
getTemplateInputNames, shouldRenderSlotAsText, inlineFieldType,
fileInputLabel, fileMetadata) and the dead
`.home-hero__prompt-slot-control/input/select/toggle/file/text` CSS
rules.

Verified:
- pnpm --filter @open-design/web typecheck
- vitest run on HomeHero.plugin-picker, HomeHero.rail, and
  HomeView.prefill (29/29 pass; tests updated to reflect the new
  read-only span + always-on form contract)
- Manual click-to-edit on Slide deck and Image chips in the
  pnpm tools-dev web runtime — caret now lands where the user
  clicked.
2026-05-17 23:18:28 +08:00
Nicholas-XiongandGitHub 5937a648cb fix: rename Pets settings entry from 'Customize in Settings' to 'Manage Pets' (#1939)
* fix: rename Pets settings entry from 'Customize in Settings' to 'Manage Pets'

- Change pet.composerOpenSettings label to 'Manage Pets'
- Improves clarity and discoverability of Pets management feature
- Label now accurately reflects the destination (Pets settings tab)
- Fixes #1250

* fix: remove pet.composerOpenSettings overrides from non-English locales

All locales now inherit the updated 'Manage Pets' label from en.ts

* fix: add pet.composerOpenSettings to zh-CN locale

zh-CN.ts does not use ...en spread, so it needs explicit key definition

* fix: restore pet.composerOpenSettings translations in all locales with 'Manage Pets' equivalent

Instead of falling back to English, each locale now has the proper translation:
- ar: إدارة الحيوانات الأليفة
- de: Haustiere verwalten
- es-ES: Administrar mascotas
- fa: مدیریت حیوانات خانگی
- fr: Gérer les animaux
- hu: Háziállatok kezelése
- id: Kelola Hewan Peliharaan
- it: Gestisci animali
- ja: ペットを管理
- ko: 펫 관리
- pl: Zarządzaj zwierzętami
- pt-BR: Gerenciar pets
- ru: Управление питомцами
- th: จัดการสัตว์เลี้ยง
- tr: Evcil Hayvanları Yönet
- uk: Керувати вихованцями
- zh-TW: 管理寵物
2026-05-17 23:16:37 +08:00
Ethan GuoandGitHub 0d2c87242f fix(web): snapshot pending before reload so expired-auth cancel actually fires (#1972)
reloadConnectorStatuses commits its prune setState (and runs the connectorAuthorizationPendingRef mirror effect) between the two awaits in onFocus, so cancelStaleAuthorizations was reading a post-prune ref and never identified anyone as stuck. Capture the snapshot inside onFocus before reloadConnectorStatuses runs, and pass it explicitly so the expired-auth daemon cancel actually fires.

Refs #1909 #1354
2026-05-17 23:13:53 +08:00
d36ceacf3a feat(web): surface saved Project instructions for review and retrieval (#1933)
Saved Project instructions had no post-save read-back surface. The
header control was a bare pencil icon that opened an editor and closed
on Save, so after returning to the workspace a user could not confirm
what was stored, revisit it, or tell whether it was still active
(#1822).

Make the saved state discoverable: once instructions exist, the header
affordance becomes a persistent "Project instructions" chip. Opening it
shows a read-only review panel with a preview of the saved text and an
"Active - included in every message" status, plus a reopen-to-edit
action. Saving lands back on the review panel so the stored value is
read back immediately. The empty state keeps the pencil and opens the
editor directly.

Persistence and project-level system-prompt injection are unchanged.

Closes #1822

Co-authored-by: multica-agent <github@multica.ai>
2026-05-17 23:07:25 +08:00
647433ccef Fix Inspect preview transport flash (#1967)
* Fix inspect preview transport flash

Co-authored-by: multica-agent <github@multica.ai>

* Keep inactive srcdoc transport lazy

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-17 22:30:37 +08:00
8562de404a fix: horizontal overflow pushes chat panel and toolbar off-screen when iframe preview exceeds viewport width (#1930)
Co-authored-by: 郑惠 <14549727+felicia-study@user.noreply.gitee.com>
2026-05-17 20:56:52 +08:00
张东明andGitHub e93583c550 fix(web): remove overscroll-behavior:none blocking scroll in New Project panel (#1945)
`overscroll-behavior: none` on `.newproj-body` prevented trackpad and
mouse wheel scrolling on macOS — the panel only scrolled when hovering
directly over the native scrollbar track. Removing the property restores
normal scroll behavior.

Fixes #1942
2026-05-17 20:28:22 +08:00
kamiandGitHub 30ad8b8ac3 improve privacy consent modal: policy link, clearer CTAs, mobile layout (#1921)
The first-run consent banner had no link to a privacy policy, an
affirmative button ("Help improve") that didn't read as a consent
choice, and a fixed bottom-right card that crowded content on phones.

- Add a "Read the privacy policy" link (external-link icon, accent,
  underlined) above the actions, plus a root PRIVACY.md it points to
  documenting the telemetry behaviour the modal discloses.
- Rename the CTAs to "Share usage data" / "Don't share" so both name
  the action; they stay equal-prominence per the EDPB/GDPR comment.
- Stretch the banner to a bottom-edge bar under 540px with a
  safe-area inset so it clears mobile browser chrome.
- Add PrivacyConsentModal tests; sync the new i18n key to every
  locale and update the consent-label assertion in App.connectors.

Refs #1756
2026-05-17 20:24:15 +08:00
Ethan GuoandGitHub 9cc1fb28f3 fix(web): apply Tweaks palette shift to :root CSS custom properties (#1952)
Walk document.styleSheets so chromatic custom properties on :root/html/body/:host (including @media-nested rules) are hue-shifted via inline setProperty, not just the values returned by getComputedStyle.

Fixes #1393
References: PR #1643 (open, in review) binds the Tweaks toolbar toggle to artifact `.tw-panel` panels via a new `injectTweaksBridge` in `srcdoc.ts` — adjacent surface but different problem. Our fix extends `injectPaletteBridge` to also shift `:root` CSS custom-property colors, which #1643 does not touch.
2026-05-17 20:16:57 +08:00
Ethan GuoandGitHub b7fa1d9286 fix(web): recover stuck Composio connector when auth is cancelled (#1909)
* fix(web): recover stuck Composio connector when auth is cancelled

When a user closes the Composio system-browser auth window without completing the flow, the connector card stayed in its loading state forever because nothing told the daemon the request was abandoned.

The window-focus refresh now awaits the status poll and, for every connector still in connectorAuthorizationPending whose freshly fetched status is not 'connected', issues cancelConnectorAuthorizationRequest and clears the matching local pending/error UI state. The connected guard is read from the just-returned statuses so a postMessage success that lands just before focus is not racy.

Fixes #1354

* fix(web): gate Composio focus auto-cancel on TTL + cancel response

Require expiresAt to have elapsed before auto-cancelling a pending
Composio auth on focus, and preserve the spinner on a failed cancel
(matching the manual Cancel handler) so the UI never disagrees with
the daemon.

Refs https://github.com/nexu-io/open-design/pull/1909#discussion_r3254152045
Refs https://github.com/nexu-io/open-design/pull/1909#discussion_r3254152046
2026-05-17 20:15:38 +08:00
Ethan GuoandGitHub 324eca27ea feat(web): add manual removal for captured Pod components (#1951)
* feat(web): add manual removal for captured Pod components

Adds `removePodMember` helper and a per-chip × in `BoardComposerPopover`; leaves `comments.ts` untouched (avoid-zone from #1127).

Closes #802
Contract: runs/2026-05-16T08-08-52_open-design_issue-feat/contract.md

* style(web): hide Pod chip × until chip hover

Swaps the unicode × for the existing `Icon name="close"` SVG so the
hit target stays centered, and fades the button in only on chip
hover / keyboard focus for a quieter resting state.

* fix(web): auto-close Pod composer when last chip is removed

Removing the last chip leaves a stale anchor; close so Send cannot attach to elements no longer visible.

* refactor(web): extract BoardComposerPopover and Pod-member reducer

Moves the popover to its own module and lifts the chip-removal reducer into a pure `applyPodMemberRemoval` so unit tests exercise the real code path and the popover's export is no longer test-only.

* fix(web): rebuild Pod anchor when a member is removed

Without this, the popover keeps the original union bbox / selector / label after each chip removal, so a subsequent Send to chat anchors the comment to elements no longer in the Pod.

* fix(web): render every captured chip and scroll on overflow

The previous slice(0, 6) cap left chips beyond the sixth invisible and undeletable. Render the full list inside a 132px-tall scrollable strip.
2026-05-17 20:13:56 +08:00
Ethan GuoandGitHub 10c62bf6e4 fix(web): warn before editing a built-in skill creates a shadow (#1850)
Clicking Edit on a built-in skill silently issued PUT /api/skills/:id,
which the daemon stores as a user-owned shadow and then hides the
built-in entry from the Settings list. From the user's perspective the
row they just edited disappears with no explanation.

Arm an inline override-warning banner on Edit for source==='built-in'
skills, mirroring the existing inline delete-confirm pattern. The edit
form only opens after the user explicitly confirms. User skills bypass
the warning and edit directly. No daemon or listSkills contract change.

Fixes #1378
2026-05-17 18:30:45 +08:00
Ethan GuoandGitHub a6288cec3f fix(web): allow editing existing routines from RoutinesSection (#1884)
The Routines list only exposed Run now / Pause / History / Delete, so any
change to a routine required deleting and recreating it. The daemon already
serves PATCH /api/routines/:id; only the web entry point was missing.

Add an Edit button on each routine row that pre-populates the create form
from the stored schedule and target via a new formFromRoutine helper, and
branch the submit handler on editingId so it PATCHes /api/routines/:id
with the updated fields instead of POSTing a new routine.

Fixes #1373
2026-05-17 15:02:54 +08:00
张东明andGitHub bac56415a2 fix(web): surface daemon error messages for invalid folder imports (#1923)
* fix(web): surface daemon error messages for invalid folder imports

importFolderProject() was swallowing non-2xx responses by returning
null, so the UI could only show a generic "Open folder failed: <path>"
message even though the daemon already returns specific errors like
"cannot import the filesystem root" or "folder not found".

Parse the daemon error body and throw so the panel displays the actual
reason. Also show feedback for empty path input instead of silently
returning.

Fixes #1186

* test(web): update folder import test to match new error propagation

The existing test expected a generic "Open folder failed: <path>"
message from a boolean return. Update to match the new behavior where
the daemon's error message is thrown and displayed directly.
2026-05-17 15:00:49 +08:00
Nicholas-XiongandGitHub 330a93116d fix: prevent horizontal overflow in split layout when iframe content exceeds viewport width (#1935)
Fixes #1929

Problem:
- When viewing HTML artifacts in preview mode, if iframe content width exceeds
  the available workspace width, the entire .split grid container expands
  beyond the viewport
- This pushes the left chat panel and top toolbar buttons outside the visible area
- Users cannot access chat or toolbar when viewing wide preview content

Root Cause:
- .split uses grid-template-columns: 460px 8px minmax(400px, 1fr)
- The third column minmax(400px, 1fr) expands to fit content
- No max-width or overflow constraint on .split container
- When iframe content is wider than viewport, grid expands to accommodate it
- Parent containers don't constrain the grid width

Solution:
- Add max-width: 100% to .split to constrain it to parent width
- Add overflow: hidden to prevent content from pushing layout
- Grid columns now stay within viewport bounds
- Only the iframe/preview area scrolls internally

Changes:
- Modified: apps/web/src/index.css
  - Added max-width: 100% to .split (line 1491)
  - Added overflow: hidden to .split (line 1492)

Impact:
- ✅ Fixes horizontal overflow when iframe content exceeds viewport width
- ✅ Chat panel stays visible and accessible
- ✅ Toolbar buttons remain in their intended positions
- ✅ Layout stays fixed to viewport width
- ✅ Only preview area scrolls internally
- ✅ No breaking changes to existing layouts
- ✅ Works with all viewport sizes

Testing:
1. Open any project and generate an HTML artifact (e.g., todo app)
2. Open the artifact in preview mode (desktop viewport)
3. Verify the layout stays within viewport width
4. Verify chat panel remains visible on the left
5. Verify toolbar buttons remain accessible at the top
6. Verify only the preview iframe scrolls horizontally if content is wide
7. Test with various artifact widths (narrow, normal, extra wide)
8. Test resize handle still works correctly

Before Fix:
- Wide iframe content pushes entire layout beyond viewport
- Chat panel disappears off-screen to the left
- Toolbar buttons disappear off-screen
- Horizontal scrollbar appears on entire page
- Poor user experience with wide content

After Fix:
- Layout stays fixed to viewport width
- Chat panel always visible
- Toolbar always accessible
- Only iframe scrolls internally
- Clean, predictable layout behavior
2026-05-17 14:57:33 +08:00
Nicholas-XiongandGitHub d54d3412d6 fix: wire onRenameProject through EntryShell to DesignsTab (#1882)
Fixes #1790

## Problem

Project rename does not take effect in Open Design 0.8.0. After entering
a new name and clicking OK, the project keeps its old name.

## Root Cause

The onRenameProject callback was broken in the prop chain:

App.tsx ──(handleRenameProject)──▶ EntryView ✅
EntryView ──(onRenameProject missing)──▶ EntryShell ❌
EntryShell ──(onRename not in Props)──▶ DesignsTab ❌
DesignsTab.commitRename() → onRename?.() → silent no-op ❌

EntryView received onRenameProject from App but never forwarded it to
EntryShell. Since EntryShell is what renders DesignsTab, DesignsTab
never received the onRename callback. Because the prop is typed optional
(onRename?:), the confirm handler's onRename?.(id, trimmed) call
silently no-ops — no error, no rename.

This was introduced when EntryShell was added to the 0.8.0 codebase as
an intermediate shell layer and the rename prop was missed during the
handoff.

## Solution

Wire the callback through the missing links:

1. EntryView.tsx: Pass onRenameProject={onRenameProject} to EntryShell
2. EntryShell.tsx:
   - Add onRenameProject to Props interface
   - Destructure it in function params
   - Pass onRename={onRenameProject} to DesignsTab

Now the callback chain is complete and project renames work correctly.

## Testing

Verified that:
- Project rename dialog accepts new name
- Clicking OK successfully renames the project
- Project list reflects the new name immediately
- No console errors or warnings

## Impact

3 lines changed across 2 files. Low risk — only restores the missing
prop forwarding, does not change any rename logic or validation.
2026-05-17 14:57:11 +08:00
e64f1d8497 fix(desktop): export PDF saves to a file instead of the OS print dialog (#1920)
* fix(desktop): export PDF saves to a file instead of the OS print dialog

The `od:print-pdf` IPC handler called `webContents.print()`, which
opens the printer-first macOS system print dialog. That handler is the
destination of the renderer's `window.__odDesktop.printPdf()` bridge,
so on desktop "Export PDF" felt like a print flow rather than a file
export — from both the PreviewModal share menu (which always uses this
path) and the FileViewer share menu (whose daemon-route fallback lands
here too).

Route the handler through a direct Save-as-PDF flow instead: a native
Save dialog, then `webContents.printToPDF()` straight to the chosen
file — the same shape `exportPdfFromHtml` already uses for the
daemon-backed export path. The flow is extracted into
`savePrintReadyDocumentAsPdf` behind a structural `PrintReadyPdfTarget`
surface that has no `print()` method, so the regression cannot be
reintroduced.

Fixes #1774

Co-authored-by: multica-agent <github@multica.ai>

* fix(desktop): preserve PDF page sizing in print bridge

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-17 11:26:08 +08:00
Nicholas-XiongandGitHub a766048354 fix: Localize skill validation messages in Chinese and all other languages (#1690)
* fix: Localize skill validation messages in Chinese and all other languages

Fixes #1377

Skill creation form validation messages now appear in the user's selected
language instead of always showing English.

**Before:**
- App language set to Chinese
- Leave skill name or body empty
- Validation shows: 'Skill name is required.' (English)
- Mixed-language experience

**After:**
- App language set to Chinese
- Leave skill name or body empty
- Validation shows: '技能名称为必填项。' (Chinese)
- Consistent language experience

**Implementation:**
- Added 'settings.skillsNameRequired' translation key (18 languages)
- Added 'settings.skillsBodyRequired' translation key (18 languages)
- Updated SkillsSection.tsx to use t() instead of hardcoded strings
- Covers: en, zh-CN, zh-TW, de, fr, es-ES, pt-BR, ru, ja, ko, ar, fa, id, pl, hu, uk, tr, th

* fix: Remove line number prefixes from i18n locale files

The previous commit accidentally included line numbers from read_file output
in the locale files, causing TypeScript compilation errors.

This commit removes all line number prefixes (e.g., '1400|') from the
skillsNoFiles entries in all 17 language files.

* fix: Escape apostrophes in Ukrainian skill validation messages

Fix TypeScript syntax error caused by unescaped apostrophes in Ukrainian
locale strings for skill name and body validation messages.
2026-05-16 16:30:58 +08:00
Yuhao ChenandGitHub 2f553941d3 fix(runtime): auto-annotate imported HTML elements for Tweaks selection (#892) (#1169)
* fix(runtime): auto-annotate imported HTML elements for Tweaks selection (#892)

* fix(runtime): always annotate missing od-ids and broaden selector (#892)

- Remove the conditional gate so annotateMissingOdIds runs for every
  srcdoc, not just comment/inspect bridges. This fixes the persistence
  regression where saved Inspect tweaks reference synthesized data-od-id
  selectors that vanish when the bridge is rebuilt without annotations.

- Expand the selector to cover div-based imported HTML (div[class],
  div[id]), headings, buttons, and links so Picker/Pods work on
  common anonymous wrapper markup.

Addresses review feedback from lefarcen and mrcfps.

* fix(runtime): narrow div selector, skip iframe/object/embed, add tests (#892)

- Replace broad div[class] with direct-child combinators only under
  semantic containers, body, and [id] elements to avoid layout noise
- Add iframe, object, embed to the skip list alongside script/style
- Add tests for: direct-child divs, nested div skip, always-on behavior,
  skip list with id attributes, div children of [id] elements
- Format selector as array join and skip tags as Set for readability

* fix(runtime): let annotateManualEditSourcePaths coexist with data-od-id (#892)

annotateMissingOdIds now runs unconditionally, so elements like main/h1
get data-od-id before annotateManualEditSourcePaths runs. The old skip
condition (has data-od-id) incorrectly prevented source-path marking on
those elements. Change the guard to skip only when the element already
has data-od-source-path, allowing both attributes to coexist.
2026-05-16 16:17:36 +08:00
Yuhao ChenandGitHub 26502ea124 test(web): decouple memory preview icon assertion (#1863) 2026-05-16 11:37:34 +08:00
epicandGitHub 1896c699a8 fix(web): use wrapper+icon pattern for agent model select to fix duplicate chevrons (#1831)
Native macOS browser sometimes renders its own disclosure indicator even
with appearance:none, stacking it on top of the CSS background-image SVG
chevron. Apply the same orbit-template-select-wrap pattern: clear the
background-image via explicit wrapper class, use an absolutely-positioned
Icon component for a single, controlled chevron. Affects both the model
picker and reasoning picker selects in the agent-model-row.
2026-05-16 00:29:53 +08:00
mehmet turacandGitHub 6b15236843 fix(web): distinguish expanded memory preview action (#1813) 2026-05-15 23:08:30 +08:00
Abhisek DasandGitHub 3f56395e2d fix(web): restore Settings subtab-pill hover contrast in dark theme (#1815)
The .subtab-pill button hover rule hard-coded `rgba(255,255,255,0.6)` as the
hover background. In light theme this composites to a near-white wash that
sits cleanly under `--text`, but in dark theme it overlays a translucent
white on the dark panel and produces a bright, near-white surface beneath
the same light `--text`, dropping contrast to ~1.87 (well below WCAG AA 4.5).

The reporter (#1795, on 0.6.0) called this out on five Settings surfaces.
Four of them — BYOK / Appearance / Notifications / protocol-chip — were
incidentally fixed by the c5d77a03 dark-palette refresh because they use
the .seg-control / .seg-btn hover path, not the .subtab-pill path. The
Pet → pet source tabs (Built-in / Custom / Community) still reproduce on
0.7.0 because they use .subtab-pill, which never got a theme-aware hover.

Switch the hover background to `var(--bg-muted)`, an existing token that
mirrors `--bg-panel`'s active state shape (one step lighter than
`--bg-subtle`) and is defined per-theme. Composited contrast becomes
~9.4 in dark and ~14.3 in light against `--text`, comfortably above AA.

The same .subtab-pill rule is shared by RoutinesSection and DesignsTab, so
they pick up the fix as a side effect of fixing the source class.

Anchored by a Playwright regression test under e2e/ui/ that asserts WCAG
AA across both themes for the Pet source tabs and the seg-btn surfaces.
The test goes red on this commit's parent (Pets/dark, ratio 1.87) and
green here (all four assertions pass).
2026-05-15 23:08:00 +08:00
Yuhao ChenandGitHub 19d65678a3 fix(web): keep filter pill hover labels readable (#1828) 2026-05-15 23:07:43 +08:00
mehmet turacandGitHub 9689dce1ad fix(web): align comment marker numbering (#1826) 2026-05-15 23:07:32 +08:00
mehmet turacandGitHub 444f7d03eb fix(web): reveal memory editor after edit click (#1827) 2026-05-15 23:07:18 +08:00
mehmet turacandGitHub 7d9488300e fix(web): keep draw overlay scrollable (#1848) 2026-05-15 23:05:49 +08:00
Quang DoandGitHub 13c8bc4193 feat(daemon): add OpenAI-compatible media providers (#1712)
* feat(daemon): add openai-compatible media providers

* fix(web): sync media registry with routed providers
2026-05-15 23:05:03 +08:00