mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
codex/ci-test-optimization
304
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
8a629eb999 | fix: discover codex models from cli (#2082) | ||
|
|
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> |
||
|
|
24fe033f85 |
Make ACP model detection timeout configurable (#2072)
Adds OD_ACP_TIMEOUT_MS for the ACP model-detection probe while preserving the default timeout, supporting the existing 0-disables-timeout convention, and capping oversized values before scheduling timers. Validation: CI and nix-check were green on PR head
|
||
|
|
098bbd55f4 |
Fix GitHub rate-limit fallback for marketplace plugins (#2064)
Falls back from GitHub Contents API 403/429 responses to the existing codeload tarball install path for marketplace plugin subpath sources, with regression coverage for successful fallback and failure reporting. Validation: CI and nix-check were green on PR head
|
||
|
|
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
|
||
|
|
7975514b3d | Add normal artifact creation via MCP and CLI (#2057) | ||
|
|
0101a09b10 | fix(mcp): support no-auth local HTTP servers (#2008) | ||
|
|
832bdeb535 | Centralize daemon startup (#2054) | ||
|
|
1f66c53203 |
feat(daemon): consume component manifests (#2053)
* feat(design-systems): extract component manifests * feat(daemon): consume component manifests --------- Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local> |
||
|
|
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 |
||
|
|
2e97ac9e68 |
Pin daemon data dir in agent runtime env (#2005)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
91be2696dd |
Persist routine failure reasons (#1963)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9ce504b933 | Improve Claude endpoint connection diagnostics (#1955) | ||
|
|
fb57ef2fc3 | Improve DeepSeek TUI diagnostics (#1940) | ||
|
|
2d1a7bd727 |
fix(daemon): isLocalSameOrigin honors OD_ALLOWED_ORIGINS in reverse-proxy deployments (#1902)
Closes #1868 When the daemon runs behind a reverse proxy (Nginx, Caddy, Traefik), the Host header the daemon observes is the proxy's upstream address, not the browser-visible origin the user typed in. As a result the host check inside isLocalSameOrigin rejected requests whose Origin header was *explicitly listed* in OD_ALLOWED_ORIGINS — defeating the documented escape hatch for proxy deployments. Add a single early-return in isLocalSameOrigin: if a non-empty Origin header exactly matches an entry in OD_ALLOWED_ORIGINS, trust it. The configuredAllowedOrigins parser already requires the origin to be a well-formed http(s) URL and rejects unsupported schemes, so the allow-list is a maintainer-controlled set of trusted origins. The existing host-based loopback / private-LAN path remains the default for no-Origin and unlisted-Origin requests; this change is strictly additive for the explicit allow-list case. Validation: - New tests in apps/daemon/tests/origin-validation.test.ts cover the nginx-upstream case (Host is a container IP, Origin is the allow-listed proxy origin) plus three negative cases: foreign Origin still rejected, no-Origin behavior preserved, trailing slash mismatch rejected. - pnpm --filter @open-design/daemon test -- tests/origin-validation.test.ts → green - pnpm guard → green Co-authored-by: nicejames <nicejames@gmail.com> |
||
|
|
0e61313347 | fix(prompts): stabilize discovery brand answers (#1861) | ||
|
|
70e5ea6f70 |
Removed - sentinel for Qwen CLI (#1332)
* Removed - sentinel for Qwen CLI * Addressed reviewer's comments * Removed IDE config file * Removed IDE file |
||
|
|
7eb1e7dcfe | fix(daemon): wrap Claude connection smoke stdin (#1844) | ||
|
|
9a64fccdc0 |
fix(security): resolve hostname before approving external API base URLs (#1176)
* fix(security): resolve hostname before approving external API base URLs
Before this change the daemon-side base-URL validator only inspected the
literal hostname string. A public DNS record that points at an internal
address ('internal.example.com' → 10.0.0.5) passed validation, and the
daemon would issue the upstream request anyway — turning the BYOK proxy
into an SSRF primitive against internal infrastructure.
Add a small companion ('validateBaseUrlResolved') that runs the existing
sync check, resolves the hostname with 'dns.lookup({ all: true })', and
re-applies the block-list against every resolved address. Wire it into
the wrapper the daemon already uses ('validateExternalApiBaseUrl'), so
every proxy/finalize handler picks it up without further edits.
Carve-outs match the existing sync validator:
- Loopback hostnames skip DNS (Ollama-style local LLMs still work,
including '*.localhost' / 'lvh.me'-style names that resolve to 127.0.0.1
per RFC 6761).
- IP literals are already vetted by the sync pass; no need to re-resolve.
- DNS resolver errors fall through to the existing fetch error path —
a transient ENOTFOUND should not turn into a 403.
The 6 callers that previously consumed the sync result now 'await' the
async wrapper. All call sites are already inside async route handlers.
Vitest coverage in apps/daemon/tests/connection-test.test.ts covers:
sync rejection passthrough, loopback / IP-literal short-circuits,
private IPv4 and IPv6 resolution, dual-stack with one private record,
public→public passes (api.openai.com), '*.localhost' resolved→loopback,
and resolver-error fallback.
Detected by: aeon (manual review + semgrep + osv + trufflehog).
Class: CWE-918 (SSRF) — DNS-based bypass.
* fix(security): cover remaining daemon baseUrl fetch surfaces + Ollama redirects
Addresses PR #1176 review feedback (lefarcen / mrcfps): the resolved-IP
wrapper only covered the proxy/finalize routes, leaving three adjacent
SSRF gaps open.
- testProviderConnection (/api/test/connection provider mode): switch
from sync validateBaseUrl to await validateBaseUrlResolved so a
hostname that resolves to a private IP is rejected before the daemon
POSTs the smoke prompt upstream.
- listProviderModels (/api/provider/models): same swap. Import the
DNS-aware helper from ./connectionTest.js since it carries the dns
binding the daemon owns; contracts stays pure.
- Ollama proxy stream fetch: align with the other four proxy routes
by setting redirect: 'error', so a validated public host cannot 3xx
the daemon to a private/internal URL after the pre-fetch check.
Regression coverage:
- POST /api/provider/models — DNS spy returns 10.0.0.5 for a synthetic
hostname; route must respond { ok: false, kind: 'forbidden' } and
must not invoke upstream fetch.
- POST /api/test/connection provider mode — same shape.
- /api/proxy/ollama/stream — fetch mock asserts redirect: 'error' on
the upstream Ollama call.
The existing /api/provider/models timeout test now stubs dnsPromises
so it doesn't race the probe timer against real DNS.
---------
Co-authored-by: aeon <aeon@aaronjmars.com>
|
||
|
|
13c8bc4193 |
feat(daemon): add OpenAI-compatible media providers (#1712)
* feat(daemon): add openai-compatible media providers * fix(web): sync media registry with routed providers |
||
|
|
7c1d9e9634 | fix(claude-design-import): restore notched wheel zoom + warn on regex drift (#1814) | ||
|
|
91195bc4ac |
Merge origin/preview/v0.8.0 (PR #1830) into sync branch
Pulls in the teammate's PR #1830 — e2e align to redesigned entry flows + watcher and codex runtime test fixes. Conflicts resolved: - apps/daemon/tests/runtimes/registry-and-args.test.ts: imported both withPlatform (HEAD, used 6x) and withEnvSnapshot (theirs, used 1x); both helpers are needed in the file. - apps/web/src/components/ProjectView.tsx: kept both ChatPane-remount guards. Main's fix #1710 uses lastSyncedConversationIdRef tracking URL sync; the teammate added lastSeenRouteConversationIdRef tracking last observed route. Both refs are already defined in the file and the two checks are complementary defenses. - e2e/ui/settings-*.test.ts (5 files): took teammate's version wholesale — they replaced inline `page.goto('/') + click Execution mode` with `gotoEntryHome + openSettingsDialogFromEntry` helpers, which is exactly the PR's intent. |
||
|
|
2181eb376d |
test(e2e): align UI suites with redesigned entry flows (#1830)
* test(e2e): align critical ui tests with new entry shell * test(e2e): align ui suites with redesigned entry flows * fix(e2e): correct entry chrome page helper type * fix(daemon): stabilize watcher and codex runtime tests * fix(daemon): satisfy strict watcher option typing |
||
|
|
22a3b99a47 |
Merge origin/main into preview/v0.8.0
Sync 49 commits from main. Conflicts resolved:
- .github/workflows/ci.yml: kept v0.8.0 granular per-area gating, added main's
linux specs + release-stable.yml + release-preview.yml triggers
- .github/workflows/release-preview.yml: kept v0.8.0's full workflow over main's placeholder
- apps/web/src/components/AssistantMessage.tsx: combined v0.8.0 file-ops
summary with main's stripTodoToolGroups + suppressAskUserQuestionFallbackText
- apps/web/src/components/ChatPane.tsx: kept both new imports
- apps/web/src/index.css: kept both .msg-plugin-chip and .user-copy-btn blocks
- e2e/ui/*.test.ts: kept v0.8.0 openEntrySettingsDialog helper over main's
inline dialog navigation (UI was redesigned in v0.8.0)
- nix/package-{daemon,web}.nix: kept v0.8.0 pnpmDepsHash; rerun nix build to refresh
|
||
|
|
4e19c3f4f3 |
Prevent imported Claude canvases from zooming on scroll (#1726)
* Preserve HTML preview state across mode toggles
HTML previews could rebuild their iframe when switching into Edit or Comment, which reset scroll/canvas state and caused visible churn for multi-file artifacts. The viewer now keeps URL-loaded previews mounted when the artifact owns the mode bridge, relays file-refreshes through frame navigation, and restores preview scroll/viewport state across bridge mode changes.
Constraint: Generic srcdoc-only bridges are still required for unbridged artifacts, inspect mode, palette tweaks, decks, draw overlays, and forceInline.
Rejected: Keep all Edit/Comment previews on srcdoc | causes unnecessary iframe replacement for bridge-capable URL-loaded artifacts.
Confidence: high
Scope-risk: moderate
Directive: Do not enable URL-load for bridge-dependent modes unless the artifact has an owned postMessage bridge.
Tested: pnpm guard
Tested: pnpm --filter @open-design/web typecheck
Tested: pnpm --filter @open-design/web test
Tested: Playwright verified Edit and Comment toggles preserve iframe src and DOM node while receiving comment targets.
* Prevent preview wheel gestures from escaping into zoom
Trackpad pinch-like wheel events arrive with ctrl/meta modifiers on some platforms, which can make a normal vertical scroll feel like the preview zoomed. The preview now consumes those modified wheel events inside the host preview shell and in injected srcdoc previews, then maps the delta back to scroll where a scroll target exists.
Constraint: URL-loaded sandbox iframes cannot always be inspected by the host, so srcdoc previews need their own in-frame guard.
Rejected: Add allow-same-origin to preview iframes | weakens the sandbox boundary for generated artifacts.
Confidence: medium
Scope-risk: narrow
Directive: Do not broaden iframe sandbox permissions to fix gesture handling without a security review.
Tested: pnpm guard
Tested: pnpm --filter @open-design/web typecheck
Tested: pnpm --filter @open-design/web exec vitest run tests/components/FileViewer.test.tsx tests/runtime/srcdoc.test.ts
Tested: playwright-cli verified ctrl-wheel in preview keeps app zoom at 100% and prevents default in the iframe context
* Revert "Prevent preview wheel gestures from escaping into zoom"
This reverts commit
|
||
|
|
c18be3f130 |
fix(daemon): honor IP-literal OD_ALLOWED_ORIGINS in no-Origin Host check (#1775)
isLocalSameOrigin's no-Origin branch hard-coded the extras list to [], so a POST from a configured LAN/Tailscale host (e.g. http://100.86.154.169:7456) with no Origin header was rejected even though the operator allow-listed it. Filter extraAllowedOrigins to entries whose hostname is an IP literal (IPv4 dotted-quad or bracketed IPv6) and pass that subset to the Host-only isAllowedBrowserHost call. Named-domain entries still require an Origin header on guarded routes, preserving the existing CSRF invariant (covered by the pre-existing 'rejects no-Origin POST when Host matches a non-loopback configured origin domain' test). Fixes #1337 References: This PR takes a different approach from #1343, which was identified as introducing a CSRF security regression (CHANGES_REQUESTED by reviewers lefarcen and Siri-Ray): simply passing extraAllowedOrigins to the Host-only branch allows no-Origin requests from configured deployment hosts to bypass CSRF guards. This fix addresses the root cause with a safer approach that preserves the Host-only/no-Origin rejection invariant while enabling correct OD_ALLOWED_ORIGINS behavior for requests that include an Origin header. |
||
|
|
9cf265e520 |
feat(claude): wire AskUserQuestion tool through chat + pin TodoWrite (#1743)
* feat(claude): wire AskUserQuestion tool through chat + pin TodoWrite
Claude calls `AskUserQuestion` for mid-conversation clarifications when
the natural answer is one of a small finite set of choices. Until now
the tool round trip hit two dead ends in headless mode: claude-code -p
cannot prompt the user, so it auto-errored the tool and retried 4x;
the model then hedged by also writing the same options as a markdown
bulleted list. The host had no way to feed a real `tool_result` back.
This change makes the AskUserQuestion round trip work end to end:
* Switch Claude to `--input-format stream-json`. The daemon wraps the
prompt as a JSONL `user` message on stdin and keeps stdin OPEN, so
later writes (a `tool_result` for the open AskUserQuestion) feed
back into the same child instead of needing a fresh spawn.
* New `RuntimeAdapter.promptInputFormat()` ('text' default,
'stream-json' for Claude) so the spawn loop keeps the old close-on-
prompt behavior for every other agent.
* New `POST /api/runs/:id/tool-result` daemon endpoint and
`submitChatRunToolResult` web helper. Body carries `toolUseId` and
`content`; daemon writes a JSONL `user` message with the matching
`tool_result` content block.
* Track outstanding host answers on the run (`pendingHostAnswers`)
and close stdin on either a `usage` event or a synthesized
`turn_end` event (extracted from `assistant.message.stop_reason`
in `claude-stream`). Without the per-turn `turn_end` signal stdin
would never close after the follow up turn finished and the run
would hang until the inactivity watchdog killed it.
* System prompt: tell Claude to use AskUserQuestion for follow ups
with 2-4 finite choices, and to STOP after the tool call instead
of writing a markdown duplicate.
Web UI:
* New `AskUserQuestionCard` renders the tool input as labelled chip
buttons (single or multi select) with a Submit button styled like
the composer's Send. On submit the answer routes through
`submitChatRunToolResult` (live tool_result path) and falls back
to `onSubmitForm` (plain user message) only if the run has already
terminated. Selected chips persist across page reloads by re
parsing the stored `tool_result.content`.
* Hide markdown text that follows an AskUserQuestion in the same
turn — defense in depth against the model emitting the duplicate.
* Collapse identical `AskUserQuestion` / `TodoWrite` retries inside
any tool group to a single card. TodoWrite is a snapshot tool,
so older calls are duplicates of state.
* Pinned TodoCard above the chat composer. The latest TodoWrite
snapshot across the conversation renders once, expandable /
collapsible header, count shows in-progress + completed (1/4),
Done button dismisses when all tasks finish, soft fade gradient
above so scrolling chat text dissolves into the panel instead of
hard clipping under the card.
* Composer gains a top shadow that only appears when the pinned
todo slot sits directly above it (dark mode strengthened).
* Accordion expand / collapse motion shared between TodoCard, the
ToolGroupCard disclosure, and BashCard output via
`grid-template-rows: 0fr -> 1fr` with `cubic-bezier(0.23, 1, 0.32, 1)`
and asymmetric durations (200ms enter, 140ms exit) per Emil
Kowalski's animation framework.
* Jump-to-latest button no longer unmounts on hide; slides up with
scale 0.9 -> 1 + fade on show, slides down with scale + fade on
hide. Always horizontally centered via `margin: 0 auto`.
i18n:
* `tool.askQuestion`, `tool.askQuestionSubmit`, `tool.askQuestionPending`,
`tool.askQuestionAnswered`, `tool.todosExpand`, `tool.todosCollapse`,
`tool.todosDone`, `tool.todosDismiss` added to all 18 locales.
Unblocker:
* Fix a pre-existing render loop in `ProjectView` when the user
clicks "New conversation". `handleNewConversation` now navigates
to the fresh conversation id synchronously after
`setActiveConversationId` so the route-sync effect at L512 and
the URL-sync effect at L851 do not ping pong (route mismatch
triggered repeated reverts; React's nested-update guard fired).
* fix(claude): order turn_end after content blocks + cover chat switching
Two follow-up fixes to the AskUserQuestion + new-conversation work:
* `claude-stream.ts` emitted `turn_end` BEFORE iterating the assistant
message's content blocks. When claude-code lacks
`--include-partial-messages` (older builds), tool_use events surface
only from that loop, so the daemon's stdin-close handler saw an
empty `pendingHostAnswers` set and closed stdin before the
AskUserQuestion tool_use was even registered. The result: the model
retried, hit the same race, and gave up writing the questions in
prose. Emit `turn_end` AFTER the content loop so tool_use ids land
in `pendingHostAnswers` first.
* `server.ts` now ignores `turn_end` events with
`stop_reason: 'tool_use'`. That stop reason means the model paused
to wait for a tool execution (claude-code's internal tool runner
for Bash / Edit / Read, or a host-answered tool like
AskUserQuestion). Either way the conversation is still in flight —
closing stdin there would kill the follow-up response. Only the
natural turn-end stop reasons (`end_turn`, etc.) close stdin.
* `ProjectView.handleSelectConversation` now navigates to the picked
conversation id synchronously, mirroring the fix already in
handleNewConversation. The route-sync effect at L512 was reverting
the active conversation on every switch, ping-ponging with the
URL-sync effect at L851 until React's nested-update guard fired
with "Maximum update depth exceeded". Same bug class as the
pre-existing new-conversation render loop.
* docs(agents): capture AskUserQuestion runtime + chat UI conventions
Record the patterns this PR introduces so future contributors can find
them without spelunking server.ts:
* Agent runtime conventions — `RuntimeAgentDef.promptInputFormat`,
`run.pendingHostAnswers` / `run.stdinOpen` lifecycle, `turn_end`
ordering rule, `POST /api/runs/:id/tool-result` endpoint shape, the
Claude only system prompt block that nudges AskUserQuestion, and the
`suppressAskUserQuestionFallbackText` defense in depth.
* Chat UI conventions — URL-load vs srcDoc render mode dispatch with
bridge disqualifiers, the dual iframe visibility swap pattern,
`isOurIframe` plus the active-iframe re-check for signals that must
only come from the visible iframe, pinned TodoCard via
`PinnedTodoSlot`, count includes `in_progress`,
`dedupeSnapshotToolRetries` for AskUserQuestion / TodoWrite stacks.
* i18n keys — 18 locale files, add the key to `types.ts` first.
* UI animation philosophy — `cubic-bezier(0.23, 1, 0.32, 1)` ease out,
asymmetric 200/140ms enter/exit, accordion via `grid-template-rows`,
no `transform: scale(0)`, keep mounted + toggle class for exit
transitions instead of relying on React unmount.
* fix(claude): read promptInputFormat as field, close stdin on deferred answer
Two PR review follow-ups on the AskUserQuestion stream-json wiring.
* server.ts:4616 referenced `runtimeAdapter.promptInputFormat()` — but
`runtimeAdapter` is not declared, imported, or assigned anywhere. The
prior adapter abstraction was deleted in #1656; when the changes
were folded back into the inline handler the format was moved onto
`RuntimeAgentDef.promptInputFormat`, but this call site was missed.
`server.ts` starts with `// @ts-nocheck` so typecheck never caught
it — every chat run hit `ReferenceError: runtimeAdapter is not
defined` the moment we wrote the prompt to a stdin-fed child, which
is every agent with `promptViaStdin: true` (claude, codex, copilot,
cursor-agent, gemini, opencode, pi, qoder). Read the format off the
in-scope `def` and default missing values to `'text'`.
* `submitToolResultToRun` cleared the answered id from
`pendingHostAnswers` but never closed stdin if a `turn_end` /
`usage` event had already fired with the set non-empty (deferred
by the event handler). The child then waited indefinitely for
further input until the inactivity watchdog killed it, losing the
model's follow-up response. Close stdin on the last-answer
transition when stream-json stdin is still open.
Test: pin `promptInputFormat` for every `promptViaStdin: true` agent
so future regressions of the field-vs-method contract fail at
typecheck-adjacent test time instead of in production. The new test
asserts `typeof def.promptInputFormat` is a string (or undefined),
not a function — exactly the shape mistake the original line made.
* fix(web): keep AskUserQuestion multi-select chips selected after reload when labels contain commas
`handleSubmit` joined multi-select answers with `', '` while the
reload parser split them on `','`. The pair is asymmetric: a valid
model-generated option like `"Yes, including images"` round-tripped
as `["Yes", "including images"]`, so after a page reload the locked
question card showed the user's pick as unselected — even though the
`tool_result` content the daemon actually wrote into the run was
correct, and the model saw the right answer. Bounded to post-reload
visual state, but silently confusing.
Switch to a `- ` bullet list per option, one per line, with the
parser stripping the leading `- ` back off. Newlines never appear
inside a label so the round trip is exact. The outer pairs separator
stays `\n\n` because individual answer bodies still never contain
that double-newline.
* chore: drop accidental personal design-system file
`design-systems/foldar/DESIGN.md` was added to the AskUserQuestion
branch in
|
||
|
|
bceca66bb4 |
fix(daemon): raise ACP stage timeout to match outer chat inactivity watchdog (#1734)
* fix(daemon): raise ACP stage timeout to match outer chat inactivity watchdog The ACP per-stage silence watchdog defaulted to 180_000ms (3 min), which killed long generations mid-response (e.g. an agent silently writing a large HTML artifact). The outer chat-run inactivity watchdog is already 600_000ms (10 min) precisely because 'agents [...] spend several minutes silently writing large artifacts' (server.ts), so the inner ACP watchdog was undercutting that design intent with no env override. - Raise DEFAULT_STAGE_TIMEOUT_MS from 180_000 to 600_000. - Add OD_ACP_STAGE_TIMEOUT_MS env override resolved in server.ts and threaded into attachAcpSession via stageTimeoutMs. - Bound the override by MAX_CHAT_RUN_INACTIVITY_TIMEOUT_MS so oversized values do not get clamped to 1ms by Node's signed-32-bit delay limit. - Add red-spec tests covering the new default and the override path. * fix(daemon): treat OD_ACP_STAGE_TIMEOUT_MS<=0 as disable, not 0ms schedule Addresses review feedback from @Siri-Ray on #1734. The resolveAcpStageTimeoutMs() clamp passes 0 (or any negative value normalized to 0) straight through to attachAcpSession, which would then call setTimeout(..., 0) and fail every ACP session on the next tick. The outer chat inactivity watchdog at server.ts already treats <=0 as a disable escape hatch (see inactivityTimer); the inner stage watchdog now mirrors that semantic. - attachAcpSession now skips resetStageTimer scheduling when the effective stageTimeoutMs is <=0. - Added regression test locking in the disable semantics: a 60-minute silent period under stageTimeoutMs=0 emits no error and does not kill the child. |
||
|
|
c5d77a03bd |
Garnet hemisphere (#1769)
* feat(chat-composer): enhance mention handling and input overlay - Introduced a new overlay for inline mentions in the chat composer, improving user experience by visually indicating mentions as users type. - Updated the `ChatComposer` component to manage mention entities and integrate them into the input field, allowing for better context and interaction. - Enhanced the `AssistantMessage` component to support the display of plugin action panels based on the current project context, facilitating easier plugin management. - Refactored related components to ensure consistent handling of project files and mentions across the application. This update significantly improves the chat interaction model, making it more intuitive for users to engage with mentions and plugins. * feat(plugin-management): enhance plugin action panels and UI components - Updated the `AssistantMessage` component to include plugin action panels based on the latest project context, improving user interaction with generated plugins. - Refactored the `PluginsView` to support detailed views for available marketplace entries, allowing users to access more information and actions for each plugin. - Introduced new CSS styles for improved visual representation of plugin-related UI elements, enhancing overall user experience. - Enhanced the `listPlugins` function to include an option for fetching hidden plugins, providing more flexibility in plugin management. This update significantly improves the usability and functionality of the plugin management system, making it easier for users to interact with and manage their plugins. * fix(assistant-message): refine plugin folder candidate selection logic - Updated the `pluginFoldersTouchedThisTurn` function to improve the logic for selecting plugin folder candidates based on touched paths and message content. - Introduced a new helper function, `pathMatchesFolderFileBasename`, to enhance the matching criteria for folder candidates. - Added a check for explicit folder matches before falling back to a single candidate, improving accuracy in folder selection. - Modified the `shouldRenderSlotAsText` function in `HomeHero` to include the name parameter, refining the rendering logic for slot text. These changes enhance the functionality and reliability of the assistant message component in managing plugin folder candidates. * feat(plugin-folder-actions): implement agent-routed CLI actions for plugin management - Introduced a new `PluginFolderAgentAction` type to streamline actions related to plugin folders, including install, publish, and contribute. - Updated the `DesignFilesPanel`, `FileWorkspace`, and `AssistantMessage` components to utilize the new agent action handling, improving user interaction with generated plugins. - Refactored the action handling logic to send commands to the agent, enhancing the workflow for managing plugin folders. - Added corresponding tests to ensure the new functionality works as expected and integrates seamlessly with existing components. This update significantly enhances the plugin management experience by routing actions through the agent, allowing for a more cohesive and interactive user experience. * Fix PR 1702 CI blockers * Fix PR 1702 remaining CI checks * Prebuild AGUI adapter after install * Restore plugin project snapshot wiring * feat(marketplace): refactor marketplace URL handling and enhance fetching logic - Introduced new functions to normalize marketplace URLs and manage fetching of marketplace manifests, improving the reliability of marketplace integrations. - Updated the server and plugin logic to utilize the new fetching mechanisms, ensuring consistent handling of marketplace data. - Enhanced tests to cover new URL normalization and fetching scenarios, ensuring robustness in marketplace management. This update significantly improves the marketplace experience by streamlining URL handling and enhancing data fetching capabilities. * Fix project auto-send cleanup spec * Reconcile run messages on cancel * Use active design system as visual direction * Fix active design system prompt wording * feat(workspace-tabs): implement workspace tabs functionality and file attachment handling - Introduced a new `WorkspaceTabsBar` component to manage workspace tabs, allowing users to navigate between different views (projects, marketplace, etc.). - Enhanced file handling capabilities in the `HomeHero` and `EntryShell` components, enabling users to stage and attach files before project creation. - Updated the `App` component to support auto-sending attachments alongside the first message in a project. - Improved CSS styles for workspace tabs and attachment UI, ensuring a cohesive design and user experience. This update significantly enhances the workspace navigation and file management features, providing users with a more intuitive and efficient workflow. * refactor(workspace-tabs): streamline workspace tabs and UI components - Removed unused components and actions from the `WorkspaceTabsBar` and `AppChromeHeader`, simplifying the codebase. - Updated CSS styles for the workspace shell and tabs, enhancing visual consistency and reducing element sizes for a cleaner layout. - Introduced a new client type detection mechanism to dynamically adjust the workspace shell's class, improving responsiveness. - Added tests for the `WorkspaceTabsBar` to ensure proper navigation and tab management functionality. These changes improve the overall performance and user experience of the workspace navigation system. * Update critical e2e for entry modal flow * Stabilize entry critical e2e flows * fix(ui): adjust workspace tabs and header styles for improved layout - Updated the CSS for workspace tabs and the app header, reducing element sizes and padding for a cleaner appearance. - Introduced a new button in the `WorkspaceTabsBar` for quick access to the home tab, enhancing navigation. - Minor adjustments to the layout and styles to ensure consistency across components. These changes enhance the user interface and improve the overall user experience in the workspace navigation system. * feat(workspace-tabs): implement pinned home tab functionality - Added a new pinned home tab feature to the `WorkspaceTabsBar`, allowing the home tab to remain accessible during navigation. - Updated tab management logic to collapse duplicate home tabs into a single pinned instance when restoring from local storage. - Enhanced CSS styles for workspace tabs to accommodate the new pinned tab design. - Updated tests to verify the behavior of the pinned home tab and its interaction with other tabs. These changes improve navigation consistency and user experience within the workspace. * refactor(workspace-tabs): enhance tab management and styling - Updated CSS styles for workspace tabs, adjusting padding and flex properties for improved layout and consistency. - Refactored tab creation logic to ensure unique IDs for project and marketplace tabs, enhancing navigation clarity. - Removed deprecated functions related to pinned home tabs, streamlining the codebase. - Improved test cases to verify independent behavior of home tabs during navigation. These changes enhance the user experience by providing a more intuitive tab management system and a cleaner UI. * style(workspace-tabs): update CSS for improved layout and visibility - Adjusted CSS properties for workspace tabs, including overflow, position, and z-index to enhance layout and stacking context. - Ensured consistent styling across tab components for better visual hierarchy. These changes contribute to a more polished and user-friendly interface within the workspace. * style(entry-layout): update CSS variables for improved layout consistency - Replaced fixed width values with CSS variables for the entry rail to enhance flexibility. - Adjusted padding and height properties for better visual alignment and spacing. - Introduced a new background style for the entry main topbar to improve aesthetics. These changes contribute to a more responsive and visually appealing layout in the entry view. --------- Co-authored-by: qiongyu1999 <2694684348@qq.com> Co-authored-by: Eli <129168833+qiongyu1999@users.noreply.github.com> |
||
|
|
3d0e708720 | fix(daemon): treat media generate handoff as success (#1715) | ||
|
|
ad275dbc02 |
fix(daemon): use danger-full-access codex sandbox on Windows to unblock PowerShell (#1745)
Codex CLI's workspace-write sandbox on Windows blocks every shell invocation with 'powershell.exe ... rejected: blocked by policy', so the agent cannot list files, navigate the workspace, or call any shell-backed tool. Codex has no working OS-level sandbox on Windows and falls back to a coarse policy that rejects shell unconditionally. Switch to --sandbox danger-full-access on win32 only. macOS (Seatbelt) and Linux (Landlock+seccomp) keep workspace-write because their sandbox enforcement permits shell while restricting writes. Tests anchor the workspace-write expectations explicitly to darwin and linux via withPlatform(), and a new win32 case asserts the danger-full-access flag and that the workspace-write-scoped network config override is dropped. Fixes #1721. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
545aed642e |
Merge remote-tracking branch 'origin/preview/0.8.0' into preview/v0.8.0
# Conflicts: # nix/package-daemon.nix # scripts/postinstall.mjs |
||
|
|
76defffb93 |
Garnet hemisphere (#1702)
* feat(chat-composer): enhance mention handling and input overlay - Introduced a new overlay for inline mentions in the chat composer, improving user experience by visually indicating mentions as users type. - Updated the `ChatComposer` component to manage mention entities and integrate them into the input field, allowing for better context and interaction. - Enhanced the `AssistantMessage` component to support the display of plugin action panels based on the current project context, facilitating easier plugin management. - Refactored related components to ensure consistent handling of project files and mentions across the application. This update significantly improves the chat interaction model, making it more intuitive for users to engage with mentions and plugins. * feat(plugin-management): enhance plugin action panels and UI components - Updated the `AssistantMessage` component to include plugin action panels based on the latest project context, improving user interaction with generated plugins. - Refactored the `PluginsView` to support detailed views for available marketplace entries, allowing users to access more information and actions for each plugin. - Introduced new CSS styles for improved visual representation of plugin-related UI elements, enhancing overall user experience. - Enhanced the `listPlugins` function to include an option for fetching hidden plugins, providing more flexibility in plugin management. This update significantly improves the usability and functionality of the plugin management system, making it easier for users to interact with and manage their plugins. * fix(assistant-message): refine plugin folder candidate selection logic - Updated the `pluginFoldersTouchedThisTurn` function to improve the logic for selecting plugin folder candidates based on touched paths and message content. - Introduced a new helper function, `pathMatchesFolderFileBasename`, to enhance the matching criteria for folder candidates. - Added a check for explicit folder matches before falling back to a single candidate, improving accuracy in folder selection. - Modified the `shouldRenderSlotAsText` function in `HomeHero` to include the name parameter, refining the rendering logic for slot text. These changes enhance the functionality and reliability of the assistant message component in managing plugin folder candidates. * feat(plugin-folder-actions): implement agent-routed CLI actions for plugin management - Introduced a new `PluginFolderAgentAction` type to streamline actions related to plugin folders, including install, publish, and contribute. - Updated the `DesignFilesPanel`, `FileWorkspace`, and `AssistantMessage` components to utilize the new agent action handling, improving user interaction with generated plugins. - Refactored the action handling logic to send commands to the agent, enhancing the workflow for managing plugin folders. - Added corresponding tests to ensure the new functionality works as expected and integrates seamlessly with existing components. This update significantly enhances the plugin management experience by routing actions through the agent, allowing for a more cohesive and interactive user experience. * Fix PR 1702 CI blockers * Fix PR 1702 remaining CI checks * Prebuild AGUI adapter after install * Restore plugin project snapshot wiring * feat(marketplace): refactor marketplace URL handling and enhance fetching logic - Introduced new functions to normalize marketplace URLs and manage fetching of marketplace manifests, improving the reliability of marketplace integrations. - Updated the server and plugin logic to utilize the new fetching mechanisms, ensuring consistent handling of marketplace data. - Enhanced tests to cover new URL normalization and fetching scenarios, ensuring robustness in marketplace management. This update significantly improves the marketplace experience by streamlining URL handling and enhancing data fetching capabilities. * Fix project auto-send cleanup spec |
||
|
|
4f15c33595 | Merge remote-tracking branch 'origin/preview/0.8.0' into preview/v0.8.0 | ||
|
|
98bc6d63e6 |
feat: Critique Theater wireup (activate the stack, M0 dark-launch by default) (#1338)
* feat(web): pure reducer for Critique Theater states (Phase 7.1)
Pure CritiqueState reducer driven by the contracts-level PanelEvent
(the same shape both the live SSE stream and the recorded transcript
emit), so a single reducer powers both the in-flight panel and the
rerun replay. Lifecycle covers run_started → running → (shipped /
degraded / interrupted / failed), with panelist_open / dim /
must_fix / close / round_end events building per-round
CritiquePanelistView entries as they arrive.
Defensive behaviour that surfaced while writing the spec tests:
- Terminal phases (shipped / degraded / interrupted / failed) are
sticky against further lifecycle events for the same run, except
for parser_warning which can land late and is recorded in a side
channel without changing phase.
- A new run_started for a different runId at any time discards the
prior state and reboots, so the UI can launch consecutive runs
without an explicit reset action.
- Events whose runId does not match the active run return the same
state reference, so React's useReducer doesn't re-render
subscribers on stray traffic.
- Round bookkeeping keys by round number rather than "always last",
so an out-of-order panelist_dim for round 1 arriving after a
round 2 dim does not corrupt the round 2 bucket.
Test coverage: 18 cases covering each transition, the runId guard,
sticky-terminal behaviour, the out-of-order round invariant, and
the stable-identity guarantee. Sets up Phase 7.2 and 7.3 to wire
SSE + replay into the same reducer.
* feat(web): useCritiqueStream hook subscribes to SSE and feeds reducer (Phase 7.2)
createCritiqueEventsConnection is a pure connection manager that
mirrors apps/web/src/providers/project-events.ts: opens an
EventSource at /api/projects/:id/events, listens for every name in
CRITIQUE_SSE_EVENT_NAMES, decodes each frame back into a PanelEvent
(stripping the critique. prefix and merging the data payload), and
hands it to the caller's onEvent. Reconnect uses exponential
backoff (1s → 30s) and resets on `ready`; malformed payloads drop
with a dev-mode warning rather than tearing the stream.
useCritiqueStream wraps the manager in a useReducer that owns the
CritiqueState. enabled=false or a null projectId tears down the
connection cleanly; switching projectId closes the old connection
and opens a fresh one. The returned dispatch lets local UI
synthesise actions (e.g. an Esc keypress firing a synthetic
interrupted while a kill request is in flight); production traffic
comes from the SSE stream.
Test coverage:
- sse.test.ts (10 cases, node env): subscription set covers every
CRITIQUE_SSE_EVENT_NAMES channel; payload decoding lifts the wire
shape back to PanelEvent; malformed JSON is swallowed and does
not stop the stream; exponential backoff schedule and ready-reset
semantics are pinned with a setTimeout seam; close() cancels
pending reconnects and shuts the live source; no-op fallback
when EventSource is unavailable.
- useCritiqueStream.test.tsx (6 cases, jsdom env): idle pre-event,
reducer driven by synthetic actions, no connection when disabled
or projectId is null, clean close on unmount, projectId change
reopens cleanly.
* feat(web): useCritiqueReplay hook drives reducer from transcript file (Phase 7.3)
Fetches the per-run NDJSON transcript (one PanelEvent per line),
parses every line via the shared isPanelEvent predicate, and
dispatches into the same CritiqueState reducer the live SSE stream
uses. A single reducer means the UI rendering a replay can be
identical to the live panel, and a UI mounting both
useCritiqueStream and useCritiqueReplay in parallel does not have
to reconcile two state shapes.
speed knob is `paused | instant | live | { intervalMs: N }`.
- instant flushes every event synchronously, useful for opening a
finished run already at its terminal state.
- intervalMs paces dispatches at a fixed cadence so the reviewer
can watch the run unfold.
- paused parses the transcript but holds events back until the
caller advances speed (consumers can drive a scrubber later).
- live is reserved for the future "playback at original cadence"
feature, currently treated as instant; replay timestamps are not
yet persisted with each event so honest pacing requires a
follow-up Phase 7+ task.
gunzip seam handles `.ndjson.gz` transcripts via
DecompressionStream when present; the production fetch path picks
between text and arrayBuffer based on the URL extension. Both seams
are injectable so the unit tests don't need to spin up a real
network or a real gzip pipeline.
Test coverage (8 cases, jsdom env):
- Idle status before any URL is provided.
- speed=instant flushes the full transcript synchronously to
shipped state.
- speed={intervalMs:N} paces with the setTimeout seam, reaching
done after the last tick.
- speed=paused leaves status=playing with no dispatches.
- Empty transcript reports done with state still idle.
- Fetch rejection surfaces an error status with the message.
- Malformed NDJSON lines are skipped; valid events around them
still land.
- .gz transcripts route through the gunzip seam.
Closes the Phase 7 plan tasks 7.1 / 7.2 / 7.3 (reducer + stream +
replay), all on one branch ready for review. Phases 8+ (Theater
components) consume these from this PR.
* fix(web): close payload-override gap + paused-resume bug in Critique Theater hooks (Phase 7 review)
Two P1 fixes from lefarcen's review on PR #1307:
SSE payload override
`sseToPanelEvent` previously spread `data` after the channel-derived
`type`, so a payload-provided `type` could override the channel and
route a `critique.run_started` frame into the reducer as a `ship`
action. Reversed the spread so the channel-derived `type` is
authoritative, and revalidated the resulting object through the
contracts-level `isPanelEvent` predicate before returning. Frames
that fail validation (missing runId, empty runId, unknown type) are
dropped, so a malformed or compromised SSE frame can no longer
dispatch a wrong-shape action into the reducer.
Three new sse.test.ts cases pin the regression: hostile `type:'ship'`
in the payload still resolves to `run_started`, missing runId is
dropped, empty runId is dropped.
Replay pause/resume
`useCritiqueReplay` had one big effect keyed on `transcriptUrl`
only, so flipping `speed` from `paused` to `instant` never re-fired
and the held events sat undispatched. Split into a parse effect
(depends on URL, fetches and stores events in state) and a pace
effect (depends on parsed-events + speed, owns the cursor + timers).
The playback cursor lives in a ref that survives pause/resume
cycles, so flipping `paused` -> `instant` flushes from the current
position rather than restarting (which would double-dispatch
`run_started` and reset the reducer).
Two new useCritiqueReplay.test.tsx cases:
- paused-then-instant transitions from `playing` to `done` and
reaches the shipped terminal phase
- intervalMs paced playback dispatches one event, pauses to drain
the next scheduled timer, flips to instant, and confirms the
remaining transcript drains exactly once (cursor was preserved)
Doc consistency
The earlier source comment in useCritiqueReplay.ts claimed `live`
"paces by recorded timestamps" while the impl used zero-delay
timers and the PR body said it behaves like `instant`. Aligned to
reality: `live` currently behaves like `{ intervalMs: 0 }` (events
drain on successive microtasks via setTimeoutFn) because transcripts
do not yet carry per-event timestamps. Honest timestamp-driven
pacing is queued as a Phase 7+ follow-up.
Validated: pnpm guard, pnpm --filter @open-design/web typecheck,
Theater suite 47/47 (up from 42, +3 sse + 2 replay), full web suite
96 files / 888 tests.
* feat(i18n): seed Critique Theater key block (en + zh-CN; other locales fall back via spread)
* feat(web): Theater PanelistLane component (Phase 8.1)
* feat(web): Theater ScoreTicker component (Phase 8.2)
* feat(web): Theater RoundDivider component (Phase 8.3)
* feat(web): Theater InterruptButton component with Escape keybind (Phase 8.4)
* feat(web): Theater TheaterDegraded chip (Phase 8.5)
* feat(web): Theater TheaterCollapsed post-run summary (Phase 8.6)
* feat(web): Theater TheaterTranscript replay surface (Phase 8.7)
* feat(web): Theater TheaterStage top-level container (Phase 8.8)
* feat(web): Theater CSS using existing semantic tokens (no hex literals)
* feat(web): Theater public exports barrel
* fix(web): resolve P2 + P3 review feedback on Phase 8 (PR #1314)
Addresses all 4 P2 + 3 P3 items from codex, Siri-Ray, and lefarcen.
State-lifecycle fixes (3 x P2)
1. Reducer learns a synthetic `__reset__` action (`CritiqueResetAction`).
Host hooks dispatch it when their gating prop changes so a stale
run from a prior project / transcript cannot bleed into the next
context. Reset is idempotent on idle (returns the same reference).
2. `useCritiqueStream` dispatches `__reset__` at the top of its
connection effect, so a workspace switch from project A (which
streamed a critique) to project B clears the reducer before the
new EventSource opens. enabled=false also clears.
3. `useCritiqueReplay` dispatches `__reset__` at the top of its
parse effect, so transcriptUrl swaps (including swap-to-null after
a replay reached `shipped`) lift the reducer back to idle before
the new fetch starts.
SSE validation (1 x P2)
4. `sseToPanelEvent` now runs a per-variant `hasValidVariantShape`
check after the cheap `isPanelEvent` predicate. A
`critique.ship` frame missing `composite` / `round` / `status` /
`artifactRef` is rejected before reaching the reducer, so
TheaterCollapsed can no longer crash on `undefined.toFixed(1)`.
Every variant's required fields are validated: run_started
(protocolVersion, non-empty cast, maxRounds, threshold, scale),
panelist_* (round, role, plus variant-specific shape), round_end
(round, composite, mustFix, decision in {continue,ship}, reason),
ship (round, composite, status, artifactRef.{projectId,artifactId},
summary), degraded (reason, adapter), interrupted (bestRound,
composite), failed (cause), parser_warning (kind, position).
Reducer correctness (1 x P2)
5. `panelist_open` now materializes the round + an empty panelist
view (`{dims: [], mustFixes: []}`) so TheaterStage can highlight
the in-progress lane the instant the tag opens. Before this, a
stream that emitted only `panelist_open` after `run_started` left
`rounds = []` and the UI rendered no current round until a later
`panelist_dim` arrived.
Polish (3 x P3)
6. Brand role tint swaps from `var(--magenta, var(--accent))` to
`var(--purple, var(--accent))`. `--purple` is actually defined
across the design systems; `--magenta` is not, so Brand was
silently falling through to `--accent` and looking identical to
Designer.
7. New i18n key `critiqueTheater.interruptedSummary` for the
interrupted-collapse copy ("Interrupted at round N, best
composite X.X"). Previously the interrupted branch reused
`shippedSummary` and the UI read "Shipped at round..." for a run
that specifically did not ship. Native value in en + zh-CN; other
locales fall back via `...en` spread.
8. `TheaterDegraded` heading id comes from `useId()` instead of a
hardcoded `theater-degraded-heading`, so two chips rendered on
the same page (chat history with multiple completed runs) keep
their aria-labelledby references unambiguous.
Tests (15 new cases)
- reducer.test.ts (+5): __reset__ on running/terminal/idle, panelist_open materializes round, panelist_open does not stomp prior panelist data.
- sse.test.ts (+6): variant-level rejection for ship without required fields, degraded without adapter, run_started with empty cast, panelist_dim with non-numeric score, round_end with unknown decision, plus a positive fully-formed ship.
- useCritiqueStream.test.tsx (+2): state reset on projectId change, state reset on enabled flip false.
- useCritiqueReplay.test.tsx (+1): state reset on transcriptUrl swap to null after a replay reached shipped.
- TheaterCollapsed.test.tsx (text-pinning update): asserts the interrupted branch reads "Interrupted at round 1" + "best composite 7.9", and explicitly NOT "Shipped at round...".
- TheaterDegraded.test.tsx (+1): two chips on the same page get unique aria-labelledby ids that each resolve to an `<h3>`.
Validated
- pnpm guard clean
- pnpm --filter @open-design/web typecheck clean
- Theater suite: 13 files, 101 tests (was 86 on the first Phase 8 push, +15 new)
- tests/i18n/locales.test.ts 5 of 5 across 18 locales
* feat(web): CritiqueTheaterMount wires SSE + reducer into a single drop-in (Phase 9.1)
* feat(i18n): Critique Theater strings for de + ja + ko + zh-TW (Phase 9.2)
* fix(web): resolve P1 + P2 review feedback on Phase 9 (PR #1315)
Addresses every blocker from codex, Siri-Ray, and lefarcen. The
three state-lifecycle and SSE-validation issues they also flagged
inherit fixes from PR #1314's review pass that this branch now sits
on top of after rebase.
Real daemon kill on Interrupt (P1)
- CritiqueTheaterMount now POSTs to
/api/projects/:id/critique/:runId/interrupt alongside the
optimistic local dispatch. Before this fix, clicking Interrupt
only flipped the React state to interrupted while the daemon job
kept running. The fetch is best-effort: a 404 (endpoint not wired
yet, lands in Phase 15) is swallowed with a dev-mode console.warn
so the UI still moves to the collapsed badge.
- New fetchInterrupt test seam lets RTL assert on the URL / method
and simulate the "daemon not ready yet" path. Two tests pin both:
the happy URL proj-42/critique/run-abc/interrupt POSTs, and a
rejected fetch still flips the UI.
interruptPending reset on new run (P2)
- A ref-backed effect compares the current runId against the last
one we saw; when it changes, interruptPending is cleared. A user
who interrupts run-1 and then triggers run-2 from the same mount
now gets a fresh, enabled kill button instead of one stuck in
"Interrupting…". Pinned by a new mount test.
Escape keybind scope (P2)
- InterruptButton now checks the keydown target. Escape inside an
input, textarea, select, or contenteditable element is ignored
(and any ancestor of those via closest() is treated the same
way). Body-level focus still fires the keybind so the Theater
area's affordance keeps working. Four new tests cover textarea,
input, contenteditable, and the body-focus positive case.
userFacingName i18n key (P2)
- The spec at specs/current/critique-theater.md:6 mandates a single
critiqueTheater.userFacingName key so the "Design Jury" label can
be renamed without touching code. Phase 8 introduced
critiqueTheater.title by mistake; renamed across types.ts, en.ts,
zh-CN.ts, de.ts, ja.ts, ko.ts, zh-TW.ts, and the lone consumer
TheaterStage.tsx. The locale alignment test stays green.
Validated
- pnpm guard clean
- pnpm --filter @open-design/web typecheck clean
- Theater suite: 14 files, 112 tests (was 101 before, +11 new for
the Phase 9 review pass: 3 mount + 4 InterruptButton focus scope;
the rest were already in #1314's review fix).
- tests/i18n/locales.test.ts 5 of 5 across 18 locales.
* feat(daemon): adapter-degraded registry with TTL (Phase 10.1)
In-memory registry recording adapters that produced malformed or
oversize transcripts so the orchestrator can skip them for a TTL
window (default 24h) instead of cycling through known-bad providers
on every run.
Records carry reason (malformed_block | oversize_block |
missing_artifact), source label, and expiresAt. The test-only
clock seam lets the suite advance time deterministically and prove
that an expired entry stops counting as degraded without anyone
calling clearDegraded.
7/7 vitest cases green.
* feat(daemon): synthetic good + bad adapter fixtures (Phase 10.2)
Two test-only adapters that read the existing v1 transcript
fixtures (happy-3-rounds and malformed-unbalanced) and replay them
as either a full string or a 512-byte chunked stream. The chunked
form is what the conformance harness uses to prove the parser
holds together when the transcript arrives in arbitrary network
slices, not as one buffered blob.
* feat(daemon): adapter conformance harness (Phase 10.3)
runAdapterConformance pulls a transcript through the same
parseCritiqueStream pipeline the orchestrator uses and classifies
the outcome as shipped, degraded, or failed. On a degraded
outcome it forwards the matched reason to the adapter-degraded
registry, so a single nightly conformance run is what populates
the skip list rather than the orchestrator learning each adapter
is broken at request time.
5/5 vitest cases green covering shipped, malformed degraded,
oversize degraded, no-ship failure, and the harness-thrown
failure path.
* test(e2e): Critique Theater Playwright suite (Phase 11)
Six tests, one viewport per visual case, deterministic SSE
fixtures stubbed via page.route(). Adds the suite to
test:ui:extended so the existing extended-UI lane picks it up.
Coverage:
1. Happy path: a single mounted theater plays the full
fixture (1 run_started, 5 panelists open / dim / must_fix /
close, 1 round_end, 1 ship) and ends on the score badge.
2. Interrupt mid-run: the panelist that is open at the time
the interrupt button is clicked closes with an interrupted
marker and the transcript freezes there.
3. Visual regression at 375x720 mobile.
4. Visual regression at 768x1024 tablet.
5. Visual regression at 1280x800 desktop.
6. A11y role tree: the theater region exposes a labelled
landmark, each panelist lane is a group with an accessible
name, the score is a status live region.
All SSE traffic is stubbed by page.route so the suite runs in CI
without a daemon. The toggle is seeded via localStorage by
bootAppWithCritiqueEnabled so the gate behaves as if Settings
flipped it on. typecheck clean; playwright --list reports 6.
* test(web): reducer p99 bench at 10k iterations (Phase 13.1)
Locks the documented 2ms budget for the Critique Theater reducer
on a representative SSE script (27 actions, one full happy run)
behind a regression gate. Asserts p99 stays under 4ms (2x the
documented budget) so CI runners with a noisy neighbour do not
flake while a real regression to 20ms or 200ms still trips.
The bench is a vitest case rather than a bare microbenchmark so
it runs in the same CI lane as every other web test and does not
need a parallel runner.
* test(web): critique surface coverage walker (Phase 13.2)
Walks the public critique surface (11 SSE event names, 5 panelist
roles, 6 lifecycle phases, 9 named i18n keys) and asserts each
named symbol appears in both the src corpus and the test corpus.
The walker is the gate that catches a rename in one half of the
codebase without a matching update in the other half: a future
PR that drops 'panelist_must_fix' from the reducer without also
removing its test reference fails this suite.
62 assertions, one per symbol per corpus.
* docs: Critique Theater user guide (Phase 14.1)
Seven sections aimed at end users (not contributors):
1. What is Design Jury
2. How it works (the five panelists, auto-converging rounds,
the composite formula)
3. Settings (the M1 toggle and what it does)
4. Reading the score badge
5. Replay surface
6. Troubleshooting (degraded, interrupted, failed)
7. FAQ
The composite formula is documented as
designer * 0 + critic * 0.4 + brand * 0.2 + a11y * 0.2 + copy * 0.2
because anyone trying to reverse-engineer the score is going to
search for those weights and the docs are the place they should
land first.
* docs(daemon): critique module AGENTS map (Phase 14.2)
Daemon-side wayfinder for the apps/daemon/src/critique directory.
Tables every file, what owns what invariant, and the 'when you
change anything here' guide so a future contributor does not
have to reverse-engineer the rollout resolver before adding a
new SSE event.
* docs(web): Theater module AGENTS map (Phase 14.3)
Web-side mirror of the daemon AGENTS map. Same file table, same
invariants section, same change-impact guide, sized to the
Theater component package.
* feat(daemon): rollout flag resolver (Phase 15.1)
Single decision point every caller consults to know whether the
orchestrator should wire the critique pipeline for a given run.
Priority:
1. Skill-level policy (required wins, opt-out wins inversely)
2. Per-project override from the Settings toggle
3. OD_CRITIQUE_ENABLED env override
4. Rollout phase default
M0 dark-launch false
M1 settings only false (toggle is off until the user flips it)
M2 per-skill true if skill opted in
M3 global default true
OD_CRITIQUE_ROLLOUT_PHASE parser defaults to M0 on unknown input
so a fresh install never surprises a user with the feature on.
10/10 vitest cases green covering every cell of the matrix.
* feat(web): Settings toggle hook for Critique Theater (Phase 15.2)
React hook that reads critiqueTheaterEnabled from the existing
open-design:config localStorage blob and stays in sync via:
- the platform storage event (cross-tab)
- a open-design:critique-theater-toggle CustomEvent (same-tab)
Same-tab event is the one that fires when the Settings panel saves
in the current window: the toggle and every mounted theater update
without a page reload.
setCritiqueTheaterEnabled(next) is the imperative setter the Settings
panel calls. It preserves the rest of the stored config (mode, apiKey,
etc.) and dispatches the same-tab event after the localStorage write.
The web hook reflects what the user toggled; the daemon-side
isCritiqueEnabled is the final routing authority (project override,
env, rollout phase). When they disagree, the daemon wins for backend
gating and the web reflects the toggle state.
6/6 vitest cases green covering first read, stored read, same-tab
event flip, config preservation, corrupted JSON tolerance, and
cross-tab storage event.
* test(web): Phase 15 toggle hook failure-mode coverage (PR #1320)
lefarcen P2 on PR #1320 flagged that the PR body claimed safe
behavior for disabled localStorage, non-object JSON, and missing
CustomEvent shim, but the suite only covered corrupt JSON plus
happy-path storage events. Added four failure-mode tests so the
swallowed errors are not silently traded for a throw in a future
refactor:
1. Returns false on a stored JSON value that parses to an array
(non-object). Catches a regression where the guard treats
anything truthy as a config blob.
2. Returns false on a stored JSON value of literal 'null'.
typeof null === 'object' in JS, so the guard has to check null
explicitly; this test pins that check.
3. Returns false when localStorage.getItem throws (private mode /
disabled storage / SecurityError). The hook must swallow and
return false so the rest of the app keeps rendering.
4. setCritiqueTheaterEnabled still dispatches the same-tab
CustomEvent when localStorage.setItem throws (quota exceeded /
disabled storage). The dispatch path is the in-session
broadcast that keeps every mounted hook coherent even when
persistence is unavailable; verified by mounting two probes
and asserting both flip after the setter is called with a
throwing setItem.
10/10 vitest cases green (6 existing + 4 new).
* fix(web): honor CustomEvent payload in toggle hook listener (PR #1320)
Both Siri-Ray (blocking) and lefarcen (P2 new) caught the same
real bug in the failure-mode test I added in
|
||
|
|
640a332276 | Merge remote-tracking branch 'origin/garnet-hemisphere' into reconcile/garnet-main-merge | ||
|
|
3b7f87c7ae | Merge remote-tracking branch 'origin/main' into reconcile/garnet-main-merge | ||
|
|
8e3af79dea |
feat(github-installer): enhance GitHub content installation and error handling
- Introduced new interfaces for GitHub content entries and budgets to streamline content fetching. - Enhanced the `installFromGithub` function to support installation from GitHub contents, including subpath handling. - Implemented robust error handling and retry logic for fetching GitHub content, improving installation resilience. - Updated tests to validate the new content fetching logic and ensure correct behavior across various scenarios. This update significantly improves the GitHub installation process, making it more flexible and user-friendly. |
||
|
|
cba8bf151d | chore: align namespace lifecycle packaging | ||
|
|
b268bbe169 |
Merge origin/garnet-hemisphere (post-9e196d34) — Use Plugin handoff fix
Brings in 11 new garnet commits, most importantly: - |
||
|
|
fbcf50382a |
feat(github-installer): enhance GitHub source parsing and error handling
- Updated the GitHub source regex to support more flexible parsing of repository references, including subpaths. - Introduced a new `parseGithubSource` function to handle the extraction of owner, repo, and potential subpaths, improving the robustness of the installation process. - Enhanced the `installFromGithub` function to retry fetching from multiple candidates and provide detailed error messages when installation fails. - Added tests to validate the new parsing logic and ensure correct behavior when handling various GitHub source formats. This update significantly improves the handling of GitHub sources, making the installation process more resilient and user-friendly. |
||
|
|
53148d52c8 |
feat(media): add SenseAudio TTS provider (#1633)
* feat(media): add SenseAudio TTS provider Add SenseAudio (https://docs.senseaudio.cn) as a new TTS provider alongside ElevenLabs / MiniMax / FishAudio / Volcengine. Surfaced as the `senseaudio-tts` catalogue id, mapped on the wire to `senseaudio-tts-1.5-260319` — SenseAudio's flagship model with emotion / 多音字 / 公式朗读 / clone / text-generated voice support. Scope here is HTTP non-streaming (POST /v1/t2a_v2 with stream=false) only; SSE and WebSocket transports are intentionally out of scope. - Mirror provider + model entries in apps/daemon and apps/web registries (catalogue drift check stays green). - ENV_KEYS gets `OD_SENSEAUDIO_API_KEY` / `SENSEAUDIO_API_KEY` so the alias scheme matches every other integrated provider. - `renderSenseAudioTTS` in media.ts mirrors renderMinimaxTTS: Bearer auth, voice_setting / audio_setting body, hex-decoded audio under `data.audio`, base_resp envelope split from HTTP-level failures. - NewProjectPanel's audio supportedProviders allowlist now includes `senseaudio` so the picker actually surfaces the new entry. - Audio shape (mp3 / 32kHz / 128kbps / stereo) and default voice (`female_0033_b`) hard-coded for parity with the other TTS paths; MediaContext is unchanged. - New apps/daemon/tests/media-senseaudio.test.ts (8 specs) covers defaults, custom voice, default base URL fall-back, env-key path, missing-key error, base_resp failures, missing audio, and HTTP non-2xx — patterned on media-elevenlabs.test.ts. * docs(media): drop Chinese from SenseAudio provider comment Translate the model-capabilities line in the SenseAudio block comment (media.ts) into English. Keeping the source comments in a single language matches the rest of the daemon and avoids reviewer churn over mixed-locale prose. * fix(web): unblock openai and volcengine speech models in audio picker Per review on #1633, supportedModels()'s audio allowlist in NewProjectPanel was still filtering out gpt-4o-mini-tts (openai) and doubao-tts (volcengine) even though both are marked `integrated: true` in the shared media-models catalogue. Add the two ids so the picker matches the registry and the PR body's "alongside doubao-tts" claim holds true. * style(media): normalize speech hints to bare provider names Strip the trailing descriptions on the speech catalogue hints so every entry shows just the provider name (matching FishAudio / ElevenLabs / SenseAudio): `gpt-4o-mini-tts` → "OpenAI", `minimax-tts` → "MiniMax", `doubao-tts` → "Volcengine". Also move `gpt-4o-mini-tts` to the end of the list so the OpenAI entry sits after the upstream-focused providers, matching the recent picker grouping discussion on #1633. Mirrored in both apps/daemon/src/media-models.ts and apps/web/src/media/ models.ts; catalogue drift check + daemon (1848) + web (1150) suites all green. |
||
|
|
6b3cc61714 |
Revert "Refactor agent runtime stream handling behind adapter (#1622)" (#1656)
This reverts commit
|
||
|
|
6c16283850 |
Merge origin/main (post-7c8305f4) into reconcile branch
Brings in 10 new main commits: routine deep-link to specific conversations (#1508), Windows resource cache fix for Orbit templates, collapsible comment side panel (#1607), routines project radio polish, Copilot logo swap, and minor UI fixes. Conflicts resolved: - router.ts: garnet's home/view + marketplace routes + main's per-project conversationId deep-link field coexist on Route union - ProjectView.tsx: garnet's isPhantomDaemonRunMessage helper + main's isStoppableAssistantMessage helper both kept - ProjectView.run-cleanup.test.tsx: accepted HEAD (garnet's phantom-row regression test); main's three new tests for finalizeActiveAssistantMessagesOnStop / clearStreamingConversationMarker / shouldClearActiveRunRefs are queued as a follow-up TODO inline. |
||
|
|
d5566d7627 |
feat(daemon): user-configurable model alias for the media dispatcher (#1277) (#1309)
* feat(daemon): user-configurable model alias / redirect for the media dispatcher (#1277) Tilmirs's use case in #1277: their Doubao access has moved from `doubao-seedream-3-0-t2i-250415` to `doubao-seedream-5-0`, but the project's registered catalog still emits the old id. Every call fails because the old name no longer resolves at Volcengine. Until now the only workaround was patching the source on every update. This adds a user-configurable alias layer that swaps the catalog id for whatever wire-name the provider expects, without changing the catalog itself. Two storage layers (env wins over disk, matching the rest of media-config): 1. **Environment variable** `OD_MEDIA_MODEL_ALIASES` carries a JSON map: `'{"doubao-seedream-3-0-t2i-250415":"doubao-seedream-5-0"}'`. Single var, portable across shells (Windows cmd.exe rejects hyphens in env-var names, so the per-id pattern lefarcen suggested wouldn't have worked on Windows). Malformed JSON is tolerated — falls through to the on-disk map rather than blowing up mid-generation. 2. **media-config.json** gains a top-level `aliases` field: ```json { "providers": { ... }, "aliases": { "doubao-seedream-3-0-t2i-250415": "doubao-seedream-5-0" } } ``` The Settings UI's existing PUT writes providers only, so the writeStored path now reads the existing aliases and preserves them on every write. Without that, a Settings save would silently wipe the user's aliases. The Settings UI surface for editing aliases is a separate follow-up; manual JSON edit and the env var are the v1 entry points. The resolution happens inside `startMediaGeneration` after the catalog lookup and surface validation have already accepted the registered id, so users still get the "unknown model" error if they request a catalog id that doesn't exist. The swap only changes what the provider receives on the wire (volcengine, openai, grok, fal, nanobanana etc. each pass `ctx.model` straight into their request body). Per-provider auto-output-name and the file-naming side use the function-level `model` parameter (the catalog id), so a `.png` named after `doubao-seedream-3-0-t2i-250415` keeps surfacing the registered id the agent / CLI asked for, not the wire-level alias. `providerNote` strings include the wire name so the user can see what was actually sent. Public API additions: - `resolveModelAlias(projectRoot, modelId)` -> the wire name (or the original if no alias matches). - `readAliasMap(projectRoot)` -> { effective, env, stored } for the future Settings UI's source-attribution display. Tests - 8 new cases in tests/media-config.test.ts (suite goes 14 -> 22): pass-through, stored map, env map, env-over-stored precedence, malformed-env fall-through, coercion of bad entries (null / number / nested object / empty string / blank key), readAliasMap source attribution, and a writeConfig regression that pins alias preservation on a Settings-style provider PUT. Validated - pnpm guard clean - pnpm --filter @open-design/daemon typecheck clean (both tsconfig.json and tsconfig.tests.json) - Media test suite (media-config + media-tasks-routes + media-tasks-persistence + media-nanobanana): 33/33 Pre-existing daemon test failures on Windows (symlinks, CODEX_BIN runtime resolution, MCP config, skills, server-paths) are unrelated to this change and reproduce on a clean main checkout. * fix(daemon): preserve catalog id for capability branches, surface aliases via /api/media/config (PR #1309 review) Lefarcen + codex P2 on PR #1309: the alias swap overwrote `ctx.model` globally, which silently disabled every renderer branch that keys behaviour off the catalog id. A user aliasing `dall-e-3 -> azure-dalle3-deployment` would have the wire name swapped correctly but `body.response_format = 'b64_json'` and `body.quality = 'hd'` would no longer be set, because the `ctx.model.startsWith('dall-e-')` / `ctx.model === 'dall-e-3'` checks now saw the alias. The same regression hit the gpt-image-* size selection, the gpt-4o-mini-tts instructions branch, and the openaiSizeFor() sizing function. MediaContext now carries both fields: - `model` — the registered catalog id (`dall-e-3`, `gpt-4o-mini-tts`, `doubao-seedream-3-0-t2i-250415`). All model-family capability branches read from here. - `wireModel` — the post-alias wire name. Every `body.model = `, every URL template, and every `providerNote` string reads from here so the user sees what was actually sent and the provider gets the alias. Renderers updated: openai image (body.model + providerNote + openaiSizeFor keeps catalog), openai speech (body.model + providerNote + gpt-4o-mini-tts instructions keeps catalog), volcengine video (body + note), volcengine image (body + note + openaiSizeFor keeps catalog), grok image (body + note), grok video (body + note), nanobanana (`credentials.model || ctx.wireModel || default` chain), minimax TTS, fishaudio TTS. The MINIMAX/FISHAUDIO hardcoded maps now sit BEHIND the user alias: explicit user alias wins over the project's legacy rebranding table, then the table wins over the catalog id fallback. Stub-fallback diagnostics (the SVG placeholder + stub providerNote string) keep the catalog id since those are debug surfaces, not provider calls. Lefarcen P3: the PR description claimed readAliasMap was the daemon-public API, but the /api/media/config route returned only readMaskedConfig (which had no aliases field). readMaskedConfig now returns `{ providers, aliases: { effective, env, stored } }` so the future Settings UI PR can consume the source-attributed map directly. The `aliases` field is always present (empty maps when nothing is configured) so the UI has a stable shape to read. Tests - New `media-alias-capability.test.ts` (2 jsdom cases) drives generateMedia end-to-end with a stubbed fetch and asserts on the request body. Pins the regression: aliased dall-e-3 still sends `response_format: 'b64_json'` + `quality: 'hd'`; aliased gpt-4o-mini-tts still attaches the instructions field from the voice prop. - `media-config.test.ts` grows by 2 cases (suite goes 22 -> 24): readMaskedConfig surfaces the alias map (both env and stored sources), and the empty-state shape for fresh installs. Validated - pnpm guard clean - pnpm --filter @open-design/daemon typecheck clean (both tsconfig.json and tsconfig.tests.json) - Media test suite (config + alias-capability + nanobanana + tasks-persistence + tasks-routes): 37/37 --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
2976c76fc3 |
test: expand Memory and Routines coverage (#1521)
* test: expand settings and packaged coverage * test: extend memory settings coverage * test: cover routine settings failure states * test: cover routine operation failures * test: fix daemon test typing on CI * test: decouple packaged smoke from orbit bug * test: avoid live memory LLM calls in route tests * test: fix daemon fetch typing in CI * fix: restore preview comment and inspect toggles * test: align manual edit flow with current inspector UX * test: align comment attachment flow with current preview comments UI * fix: probe resolved Codex launch path during detection * fix: remove duplicate board activation helper after rebase * test: update ghost cli detection mock * test: align FileViewer toolbar expectation * ci: move full app tests to extended lane * ci: run app tests by changed scope * ci: cover shared app inputs in test scopes * ci: avoid setup-node cache in windows packaged smoke * test: align extended settings and manual edit flows |
||
|
|
d2738924fb |
fix(web): freeze completed run durations across conversations (#1351)
* fix(web): freeze completed run durations across conversations * fix(web): finalize stopped API runs Generated-By: looper 0.6.0 (runner=fixer, agent=codex) * fix(daemon): optimize conversation latest run lookup Generated-By: looper 0.6.0 (runner=fixer, agent=codex) * fix(web): scope streaming cleanup to conversation Generated-By: looper 0.6.0 (runner=fixer, agent=codex) * fix(web): capture streaming conversation cleanup Generated-By: looper 0.6.0 (runner=fixer, agent=codex) * fix(web): guard stale run ref cleanup Generated-By: looper 0.6.0 (runner=fixer, agent=codex) |
||
|
|
055e55abd8 |
Add batch design system testing (#1515)
* feat: add batch design system testing * fix: use daemon default agent for batch tests * fix: honor batch project prompt flags Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * fix: persist batch run output * fix: honor dry-run before daemon resolution Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * fix: persist batch assistant run ids Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * fix: cancel timed-out batch runs Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) |
||
|
|
e57e028222 |
feat(daemon): make design-system token channel default-on (PR-D) (#1544)
* feat(daemon): make design-system token channel default-on (PR-D)
Flip `OD_DESIGN_TOKEN_CHANNEL` from default-off to default-on. Every
chat that picks a brand with `tokens.css` + `components.html` siblings
(today: `default`, `kami`) now gets the structured token contract
appended to the system prompt automatically. `OD_DESIGN_TOKEN_CHANNEL=0`
keeps the DESIGN.md-only path as a kill switch.
Adds `scripts/check-design-system-flag-parity.ts`, registered in
`pnpm guard`. The guard walks every brand and asserts:
- 147 prose-only brands produce byte-identical prompts under flag-off
vs flag-on (PR-D's "no-op for legacy brands" promise)
- 2 structured brands diverge as expected (catches a future regression
that silently dropped the structured blocks)
Smoke evidence on #1385 (PR-C):
- `default` — 10/10 brand tokens used byte-for-byte in treatment vs
0/10 invented colors in control
- `kami` — treatment recovers brand name (`Kami · 纸`), the two-tier
surface (`--bg` parchment + `--surface` ivory), the CN font stack
override, and the `components.html` card pattern; control invented
"Replica" as a brand name
Co-authored-by: Cursor <cursoragent@cursor.com>
* review: address @nettee + @lefarcen feedback on parity guard
Two blocking findings from #1544 review:
1. @nettee — guard's inventory walk silently passed on unreadable
filesystem state. `fileExists` swallowed every `stat` error and the
bare `readdir` catch returned `[]` for any failure. A renamed
`design-systems/` tree, a permission-denied DESIGN.md, or a
directory at the brand path would have left `pnpm guard` happy
after checking 0 brands — exactly the silent misconfiguration this
guard exists to catch. Both error paths now treat only ENOENT /
ENOTDIR as absence and rethrow everything else, mirroring the
`readFileOptional` fix already applied to PR-C's
`apps/daemon/src/design-systems.ts`.
2. @nettee — guard exercised `composeSystemPrompt` directly, bypassing
the `process.env.OD_DESIGN_TOKEN_CHANNEL !== '0'` gate in server.ts
that PR-D actually flipped. A regression that restored `=== '1'`,
typo'd the env name, or stopped reading assets when the var is
unset would still leave the guard green. Extracted the predicate
into `isDesignTokenChannelEnabled(env)` next to
`readDesignSystemAssets` and added 6 unit tests pinning every value
that matters: unset / `'1'` / `'true'` / empty / `'0'` /
whitespace-padded. server.ts now calls the predicate. Any
regression on the env-flag semantics fails
`tests/design-system-assets.test.ts` independently of the
composer-level coverage.
Verified: pnpm guard (13/13), tsc -p scripts/tsconfig.json (clean),
@open-design/daemon typecheck (clean), 32/32 prompt + asset tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
* review: pin server-layer asset resolution end-to-end (lefarcen P2)
Round-2 review feedback from @lefarcen on #1544: the predicate suite
in tests/design-system-assets.test.ts pinned the env-flag boolean but
did NOT exercise the server prompt-assembly path that PR-D actually
flipped — the seam where the daemon decides whether to read tokens.css
/ components.html from disk and hand them to composeSystemPrompt. A
regression that, say, restored an inline `=== '1'` gate or stopped
calling isDesignTokenChannelEnabled() from server.ts would still leave
the predicate test green.
Extracted that whole seam into `resolveDesignSystemAssets(id,
builtInRoot, userInstalledRoot, env)` on apps/daemon/src/design-systems.ts.
The function combines:
1. the env-flag gate (kill switch on `OD_DESIGN_TOKEN_CHANNEL=0`)
2. the built-in → user-installed root fallback chain (per-file)
3. the DesignSystemAssets result shape consumed by composeSystemPrompt
server.ts at the prompt-assembly site is now a thin caller of this
function. The previous 13-line inline block (env check + per-file
fallback) collapses to one call, so the whole asset-resolution path
now has a single testable seam.
7 new tests in tests/design-system-assets.test.ts run the full pipeline
end-to-end against real disk fixtures:
- env unset (default-on): returns built-in assets
- env=`'0'` (kill switch): returns undefined even with files on disk
- env=`'1'` (legacy opt-in): still works
- mixed builtin/user-installed: per-file fallback merges correctly
- both halves built-in: skips user-installed roundtrip verbatim
- prose-only brand (no files): undefined / undefined
- nonexistent brand directory: undefined / undefined
Verified: pnpm guard (13/13), tsc -p scripts/tsconfig.json (clean),
@open-design/daemon typecheck (clean), 39/39 prompt + asset tests
(was 32; +7 new server-layer-resolution tests).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(test): add missing projectKind to FileViewer deck preview test
The deck preview test added in #1556 (
|