Commit Graph
2001 Commits
Author SHA1 Message Date
817885bc34 feat(amr): forward installationId to the vela CLI for analytics correlation (#3634)
* feat(amr): forward installationId to the vela CLI for analytics correlation

When the daemon spawns the vela (amr) CLI, pass this installation's id as
OD_INSTALLATION_ID once the user has consented to telemetry. vela attaches it
to its analytics events so CLI activity can be correlated back to the
open-design installation that launched it.

Resolution mirrors the /api/analytics/config handler: consent =
telemetry.metrics === true; channel-root installation.json wins over the legacy
app-config.json field. Reads are synchronous and best-effort — any missing
file / parse error / withheld consent simply omits the id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(amr): bump bundled @powerformer/vela-cli to 0.0.11

0.0.11 is the first vela CLI release that consumes OD_INSTALLATION_ID (the env
forwarded by this PR's daemon change), so bundle it to ship the installationId
analytics correlation end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(amr): mirror readAppConfig defaults in installationId forwarding

Address review: the previous helper bailed out entirely when app-config.json
was missing or corrupt, so vela correlation was skipped even though the web
analytics config (via readAppConfig) would still emit events with the same
installationId. Now match readAppConfig / applyTelemetryDefaults semantics:

- a missing/corrupt app-config.json is treated as an empty config, not a hard
  failure — we still consult the channel-root installation.json for the id;
- telemetry defaults to on (opt-out model); the id is withheld only when the
  user has explicitly set telemetry.metrics to a non-true value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(nix): refresh pnpm-deps hashes for vela-cli 0.0.11 bump

The bundled @powerformer/vela-cli bump changed pnpm-lock.yaml, so the
fixed-output pnpm-deps derivation hashes no longer matched. Apply the
CI-generated hash refresh (nix-hash-refresh artifact).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(amr): resolve installationId via shared readAppConfigSync

Address review: instead of hand-replicating readAppConfig's parsing in the
spawn-env helper (which kept diverging on edge cases like invalid telemetry
shapes), reuse the real logic. Add a synchronous mirror of readAppConfig:

- installation.ts: extract parseInstallationFile + add readInstallationFileSync
- app-config.ts: add exported readAppConfigSync (same filterAllowedKeys /
  validateTelemetry / applyTelemetryDefaults pipeline as the async path; only
  skips the best-effort migration write)
- env.ts: amrAnalyticsIdentityEnv now calls readAppConfigSync and gates on
  telemetry.metrics === true (matching analytics.ts), so vela correlation can
  no longer drift from the web analytics config for any config shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(nix): refresh pnpm-deps hashes after main merge

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:44:07 +00:00
5bcd7d2c26 fix(release-beta): pass a valid mac sign mode (on -> sign-only) (#3735)
release-beta.yml passed MAC_SIGN_MODE: "on" to build-mac.sh, which only
accepts no | sign-only | notarize and exits with "unsupported MAC_SIGN_MODE:
on". That broke every mac beta built directly from main (the green betas came
from the release-beta-isomorphism self-hosted lane, not main).

Use sign-only: signs the beta with the Apple Developer ID certificate (already
wired via APPLE_SIGNING_CERTIFICATE_*) without notarization. Notarization needs
APPLE_NOTARY_KEYCHAIN_PROFILE / the direct-credential notarize path that lands
with the release-beta-isomorphism mac-lane rework; until then this unblocks
signed mac betas from main.

Co-authored-by: audit <a@b.c>
2026-06-05 09:23:39 +00:00
c732084b23 feat: newsletter subscribe (landing entry + onboarding email capture) (#3575)
* feat: add newsletter subscribe entry points (landing + client)

Add a "Subscribe to our newsletter" form on the marketing landing page
(bottom section + dismissable bottom-right popup) and a matching popup in
the desktop client. Emails POST to a new /subscribe Cloudflare Pages
Function that writes to KV (idempotent via sha256(email)), with CORS for
the cross-origin client. i18n covers all locales; analytics fire a
newsletter_signup event.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* chore: refine newsletter copy to product-name title

Drop the frequency promise ("weekly") and the abstract "Design
intelligence" wording in favor of the product-name title "The Open Design
newsletter", and change the eyebrow label to "Stay updated" so it no longer
echoes the title. Updated across the landing page and all client locales.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* chore: ease landing newsletter popup trigger to 50% scroll / 10s

Lower the bottom-right popup trigger from 60% scroll / 25s to 50% / 10s so
it surfaces a bit sooner without interrupting the hero LCP.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* refactor(web): move newsletter signup from popup into onboarding

The desktop client's newsletter popup only mounted on the main entry view,
so first-run users (who land on onboarding) never saw it. Replace it with an
optional email field on the onboarding "About you" step instead — a more
natural, less interruptive moment to capture an address.

- Add an optional email input to the About-you step; on finish, a valid
  address is POSTed to the subscribe endpoint fire-and-forget (a slow or
  failing request never blocks completing onboarding).
- Endpoint is configurable via NEXT_PUBLIC_NEWSLETTER_URL, defaulting to the
  marketing site so dev can point at a local wrangler instance.
- Remove the NewsletterPopup component, its mount, styles, and popup-only
  analytics/i18n keys.
- Track only a boolean newsletter_opt_in on the onboarding click — the email
  address itself is never sent to analytics.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* chore(landing): mirror newsletter section into canonical template

The newsletter section/popup was added to apps/landing-page/app/page.tsx
and globals.css but the source-of-truth example.html was left stale, so
future landing/template work would regenerate against a homepage missing
the subscription entry point. Mirror the section + popup markup, styles,
and signup script into design-templates/open-design-landing/example.html
to restore lockstep per apps/landing-page/AGENTS.md.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* refactor(web): make newsletter its own onboarding step (3rd)

Promote the newsletter email from a cramped field under the About-you
dropdowns to a dedicated third onboarding step ("Stay updated"), giving it
room for a heading + explainer. Also removes the design-system creation step
from onboarding (the home sidebar entry to create a design system is
unchanged).

- steps array is now Connect / About you / Stay updated; the progress bar
  uses a 3-column grid so all three sit on one row.
- Email submit + completion now fire from the third step.
- Drop the onboarding design-system branches and their now-unused helpers
  (designItems, designSource, deriveOnboardingSourceType); a completion no
  longer carries a DS request.
- Add 'newsletter' to the onboarding analytics area/step-name enums.
- zh-CN copy: 了解你 / 订阅更新 / 获取最新动态 / 邮箱地址(选填)/ 完成.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* fix(landing): avoid logging subscriber PII when KV binding is missing

The subscribe fallback logged the full record (raw email + referer) to
provider logs whenever NEWSLETTER_SUBSCRIBERS was unbound, turning a
deploy-time misconfiguration into a privacy leak with no operator signal.
Log only non-PII metadata (hashed key, source, country) via console.warn
so the missing binding is loud without exposing the address.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* chore(i18n): rename zh-CN onboarding step 3 to 获取最新动态

"订阅更新" read awkwardly as the step label; use 获取最新动态. zh-CN only.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* refactor(web): drop dead onboarding design-system renderer prop

The newsletter step replaced the step-3 design-system surface in first-run
onboarding, leaving `renderDesignSystemCreation` typed but never destructured
or rendered in OnboardingView. Remove the stale prop through the
App -> EntryView -> EntryShell -> OnboardingView chain and document that
first-run design-system creation is intentionally no longer part of
onboarding (still reachable via the design-system-create route and the
Design Systems tab).

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* feat: sync newsletter subscribers to Resend

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-06-05 08:51:29 +00:00
CaprikaandGitHub e9f5cf9916 fix AMR OpenCode error presentation (#3729) 2026-06-05 08:50:56 +00:00
AmyandGitHub b384c64716 test(e2e): align main P0 UI coverage (#3720)
* test(e2e): align main P0 UI coverage

* test(web): align BYOK model search assertion

Generated-By: looper 0.9.2 (runner=fixer, agent=codex)

* fix: address P0 coverage review feedback

Generated-By: looper 0.9.2 (runner=fixer, agent=codex)

* fix: restore picker keyboard and recent-project coverage

Generated-By: looper 0.9.2 (runner=fixer, agent=codex)

* fix(web): repair design-system picker type checks

Generated-By: looper 0.9.2 (runner=fixer, agent=codex)

* ci: classify main CI Feishu alerts

* fix: restore active-tab P0 coverage

Generated-By: looper 0.9.2 (runner=fixer, agent=codex)

* ci: add E2E coverage reminder

* ci: run E2E coverage reminder at 18:00

* ci: avoid unsafe E2E reminder pagination exits

Generated-By: looper 0.9.2 (runner=fixer, agent=codex)
2026-06-05 08:47:16 +00:00
chaoxiaocheGitHubchaoxiaochegithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
7b52480f03 Add review-gated token contract rebuild (#3727)
* Add review-gated token contract rebuild

* Fix import token rebuild auto-queue handling

Generated-By: looper 0.9.2 (runner=fixer, agent=codex)

* Handle token rebuild import follow-ups

* Make design system revision acceptance atomic

---------

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:46:20 +00:00
556eb7f065 chore(release): bump main to 0.10.0 + clear plugin context after send (#3708 residual) (#3731)
* fix(web): clear plugin context after send

* chore(release): bump version 0.9.1 -> 0.10.0 (prep main for 0.10.0 beta from main; clears the beta anti-regression gate)

---------

Co-authored-by: yinjialu <2481044901@qq.com>
Co-authored-by: lefarcen <ontf116@gmail.com>
2026-06-05 08:35:31 +00:00
Ray XiandGitHub 2ff65a27d8 fix(web): keep in-tab presentation below workspace tabs (#3722)
* Fix in-tab presentation chrome offset

* fix(web): sync presentation overlay chrome token

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)
2026-06-05 08:22:36 +00:00
6c7b85a6d0 chore(web): re-home in-flight release/v0.10.0 fixes to main (#3716, #3710, #3662) (#3721)
* Close home mention picker before details

(cherry picked from commit 484749e33029999655b9d7fc6c148b63ad8e3dc2)

* fix image export toast offset

(cherry picked from commit 3be9082a878fdddcd6edf476a1748de9639c196b)

* Fix image export format switch save label

(cherry picked from commit c964678aba)

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
Co-authored-by: yinjialu <2481044901@qq.com>
2026-06-05 08:09:48 +00:00
2569f4eba9 feat(scenarios): od-share-to-community skill + post-completion Share button (#3657)
* feat(scenarios): add od-share-to-community plugin + Share to Open Design button

After the user finishes a generation, they currently have to re-prompt the
agent to package what they just made. This adds a quiet "Share to Open
Design" pill in the assistant-completion-row — same area as the
post-feedback Discord prompt — that injects a single trigger prompt
into the chat and lets the bundled `od-share-to-community` scenario
auto-derive the manifest from `*.artifact.json`, `brand-spec.md`, and
the workspace files instead of asking the user fields the project
files already answer.

The skill mirrors `home-hero/plugin-authoring.ts`'s constraint surface
verbatim: no chained CLI suggestions, no `jq` dependency (not in the
agent runtime baseline), and after `od plugin install --source`, point
the user at the existing 3-button plugin-folder card (Add to My plugins
/ Publish repo / Open Design PR) instead of recreating those flows
freeform — the source of bug #2332.

Verified end-to-end via headless browser:
- daemon registers the bundled scenario at /api/plugins
- button renders in the live DOM, gated on (isLast && showFeedback)
- click fires POST /api/runs with the trigger prompt as a user message
- Claude Opus picks up the prompt and starts the share flow

* fix(web): debounce share to open design action

* fix(plugins): register share community scenario pipeline

---------

Co-authored-by: kokisanai <kokisanai@users.noreply.github.com>
Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-06-05 08:09:04 +00:00
3c1a8e7bcd feat(landing-page): add html-video product page under /html-video/ (#3709)
* feat(landing-page): add html-video product page under /html-video/

Add a sister-project landing page for nexu-io/html-video and surface it in
the Product nav dropdown, mirroring the HTML Anything page.

html-video is the motion counterpart to HTML Anything: an HTML-to-video
meta-layer where a local coding agent turns a prompt, an article link, or a
GitHub repo into a multi-frame animated video, rendered to a real MP4 via the
Hyperframes engine (headless Chromium + ffmpeg). Pluggable engines, 21
templates, optional AI soundtrack, Apache-2.0.

The page is an SEO + direct-link surface, so it leads with the two primary
head terms — "HTML to video" (the category) and "Hyperframes" (the default
engine, a real search term) — across the title, H1, and section copy, and
seeds long-tail like "html to mp4" and "agent video". Content is hardcoded
EN + ZH (this is a separate repo, not a content collection); the other 16
locales derive a keyword-stacked fallback from shared i18n fragments, the
same pattern as the HTML Anything page. At-a-glance stars are pulled from the
GitHub API at build time with a snapshot fallback. Imagery is hosted on the
same R2 bucket (landing/assets/html-video/) and resized through cdn-cgi/image.

Nav: new Product dropdown entry (productMenuCopy.htmlVideoName/Blurb, added to
all 18 locales) linking to /html-video/, plus an 'html-video' active value so
the Product trigger highlights on the page.

Verified: landing-page typecheck (0 errors) and build:static — /html-video/
plus all 18 locale variants build, the Product dropdown links to it on the
home page, and the title/H1/body carry the html-video + Hyperframes terms.

* fix(landing-page): add page-local styles to html-video page

The html-video page reused the HTML Anything page's `ha-*` class names but
not its styles — those live in that page's Astro-scoped <style> block, which
does not apply across pages. Result: at-a-glance collapsed to a bullet list,
cards lost their grid, and the showcase had no layout.

Add a page-local <style> block mirroring the HTML Anything one, adapted to
this page's class names (ha-showcase, ha-surfaces, ha-roadmap-col, ha-tag).
Also fix the at-a-glance stars cell: its fallback was the word "GitHub" under
a "GitHub" label (rendering "GitHubGitHub"); the fallback is now a numeric
snapshot under a "GitHub stars" label.

* style(landing-page): restyle html-video page to Open Design system

Rework the html-video page's layout to match the rest of the marketing
site and fix spacing problems flagged in review.

- Use Open Design's global tokens (--serif / --mono / --ink / --coral /
  --line / --paper-warm) throughout instead of hardcoded values and a
  GT-Sectra fallback, so the page shares the visual system of the HTML
  Anything and Solutions pages rather than drifting into its own look.
- Tighten spacing rhythm: single-edge section padding for a consistent gap
  between sections (was doubled top+bottom margins, leaving large empty
  bands between sections), tighter title→content spacing, hairline
  dividers to group instead of wide gaps.
- Cards for the why / engines blocks (bordered, warm-paper fill) instead of
  bare stacked text; coral accent locked to one role (engine status tag,
  flow step numbers, roadmap headings, note rules).
- Fold the thin Soundtrack and Quickstart sections into a compact two-up
  notes row so they no longer read as empty sections.
- Hero image renders at its natural ratio (dropped the max-height crop that
  was cutting off the banner).
- Product menu label is "HTML Video" (title-case product name, matching
  "Open Design" / "HTML Anything") across all 18 locales.
- Remove the build-time "stats verified <date> · live where the API is
  reachable" dateline — that was internal/developer phrasing on a
  user-facing page.

* feat(landing-page): list HTML Video in the footer Products column

Add HTML Video to the Products column in both footers so the third
product in the family is reachable from the page bottom, not just the
Product nav dropdown.

- Sub-page footer (site-footer.astro): append HTML Video after HTML
  Anything in the existing Products column.
- Home-page footer (page.tsx): it had no Products column at all, so add
  one (Open Design / HTML Anything / HTML Video) before the Studio column.
- New ui.footer.htmlVideo i18n key across all 18 locales (value "HTML
  Video"; product name, not localized — same as htmlAnything).

* fix(landing-page): address html-video review feedback

- Remove the unused getCommonCopy import (derivedHtmlVideoCopy only uses
  getLandingUiCopy + getHeaderProductMenuCopy).
- Localize the grouped Soundtrack/Quickstart section's aria-label, which
  was hardcoded English ("Soundtrack and Quickstart") so every localized
  page announced that landmark in English. It now reuses the page's own
  localized section titles.

---------

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-06-05 07:22:58 +00:00
2df76ee0c9 Improve design system token import contract (#3719)
Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
2026-06-05 07:12:40 +00:00
+1 baa0f57a24 chore: back-merge release/v0.10.0 into main (reconcile QA fixes; retiring release branch) (#3715)
* chore(release): prepare 0.10.0

* [codex] Add same-run retry runtime (#3576)

* Add same-run retry runtime

* fix(daemon): route retried-start and watchdog stall through retry finalizer

Two retryable failure paths bypassed the same-run retry finalizer, so they
emitted no run_retry_attempted/run_retry_finished telemetry and could report
the fallback retry_final_result: 'not_attempted'.

- restartSameRunAfterRetry's catch now routes a failed retried start through
  finishWithRetryDecision instead of design.runs.finish, so it emits terminal
  retry telemetry and sets run.retryFinalResult. retryAttemptCount is already
  1 there, so decideSafeRunRetry suppresses with attempt_limit_reached and
  cannot loop.
- failForInactivity's no-output stall now routes through the finalizer after
  surfacing stallPayload. A silent first-token hang classifies as a retryable
  timeout at first_token_wait, so the same-run retry now recovers it. A new
  watchdogRetryRestarted guard makes the SIGTERM'd stalled child's close bail
  early (revoking only its own tool token) so it cannot re-finalize the run or
  delete the new attempt's event sink (both keyed by the shared runId).
- finishWithRetryDecision returns whether it restarted so the watchdog can set
  the guard.

Adds a run-retry-runtime test with a low OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS
proving the same-run retry fires for a silent first attempt.

* fix(web): avoid duplicate style picker group keys

* Fix Design Files and sidebar page layout regressions (#3629)

Fixes two web UI layout regressions:

- Prevent Design Files name cells from collapsing to near-zero width, which caused generated file and folder names to wrap vertically while leaving empty space to the right.
- Hide inactive entry views with display: none so Plugins, Integrations, Design Systems, and Automations no longer reserve hidden Home view height above their page content.

Validated with web typecheck, focused web component tests, pnpm guard, and local visual checks.

* fix(web): copy run error diagnostics (#3630)

* fix(web): route project file links to workspace tabs

* fix(web): preserve failed retry attempts

* Auto-open produced HTML previews (#3636)

* test(e2e): cover assistant project file link routing

* feat(web): redesign Design Files panel + pick working directory from Home (#3358) [v0.10.0] (#3618)

* feat(web): redesign Design Files panel + pick working directory from Home (#3358)

* feat(web): align Design Files panel with Claude reference UI

Redesign the Design Files panel into a continuous grouped table that
mirrors Claude's own Design Files surface: uppercase gray section bands
(Folders / Stylesheets / Scripts / Documents / Images …), flush
full-width rows with a colored icon block + bold name + type subtitle +
right-aligned timestamp, a clean single-line toolbar (breadcrumbs left,
New sketch / Paste / Upload right), a right preview card with a centered
Open action and `Modified … · size · EXT` stats, and a static
USEFUL INFO footer.

Removes the old table/sort-header/kind-filter/pagination/group-toggle
chrome and the conflicting legacy `.app .df-*` overrides in routines.css
that were trumping the panel's own stylesheet via specificity. Preserves
batch select/download/delete, rename, the row context menu,
live-artifacts, plugin-folders, drag-upload, and the reloading overlay.

Adds the designFiles.* section/kind/useful-info i18n keys across all
locales.

* feat: pick a working directory from Home before the project exists

Let users choose a code folder on the Home screen up front, instead of
only being able to replace it after a project is created. The HomeHero
composer gains a folder pill (pick / clear); the chosen path is threaded
through project creation and applied via the working-dir API once the
project id exists.

On desktop the working-dir POST is gated behind a host-minted token, so
add a `dialog:pick-working-dir` IPC handler that shows the native picker
and mints a single-use token bound to the folder, plus a
`pickHostWorkingDir()` bridge + types/normalizer in `@open-design/host`.
The renderer feature-detects the bridge and falls back to the web folder
dialog (no token) otherwise. Native dialogs now allow creating a new
folder inline (`createDirectory` / `ShowNewFolderButton`), and macOS uses
`choose folder` so the panel reliably takes key focus.

Wire the WorkingDirPill into the project composer footer and surface a
reloading state + breadcrumb root label to the Design Files panel while a
replace reindexes. Drop the WorkingDirPill "recent directories"
localStorage list in favor of a single Replace action.

* fix(web): preserve staged Home attachments when picking a working dir

Move the working-dir handoff ahead of the initial upload so staged
attachments land in the external folder instead of the temporary managed
root, and fall back to the browser folder dialog when the desktop host
build does not expose pickWorkingDir.

* fix(web): surface host pick errors and normalize Windows dir basenames

When the desktop host is present but cannot mint a working-dir token,
surface the error instead of falling back to the browser folder dialog —
the tokenless raw path would be rejected by the desktop auth gate as a
late create-time failure. Only the pure web path (no token gate) keeps
the dialog fallback. Also split the Home pill basename on both path
separators so Windows paths show the folder name, not the full path.

* fix(web,desktop): harden Home working-dir handoff against auth/token failures

Run the desktop-auth handshake before minting the Home working-dir token
so a missed startup registration fails up front instead of as a rejected
deferred POST. Surface a create-time error toast when the working-dir
handoff fails (expired ~60s token or daemon rejection) instead of
silently leaving the project in the managed root while the user believes
their folder was applied.

* fix(web): abort create on failed working-dir handoff and always show root crumb

When the pre-create working-dir handoff fails, short-circuit the rest of the
Home submit path (skip the staged-attachment upload and auto-send) so the first
run cannot proceed against the managed `.od/projects/<id>` tree the user did not
choose, and reword the error toast accordingly. Always render the Design Files
root breadcrumb, falling back to the `designFiles.crumbs` label when no
rootDirName exists, so the toolbar is not blank on the default managed-storage
view. Adds regressions for the short-circuit and default-root crumb cases.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* fix(web): normalize Windows separators in WorkingDirPill basename

shortPath() split only on '/', so a Windows working dir like C:\work\repo
rendered as the full absolute path in the footer pill instead of the
folder name. Split on both separators to match HomeHero/ProjectView, and
add a regression covering the backslash path.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
(cherry picked from commit 6408937564)

* fix(web): create files in the open Design Files folder, not the root (#3358 regression) (#3623)

#3358's panel redesign dropped the `onCurrentDirChange` handoff, so
`FileWorkspace.uploadDir` stayed at the project root while `DesignFilesPanel`
tracked the viewed folder only in local state. After navigating into a
subfolder, Upload / Paste / New sketch / dropped files all created at the
project root instead of that folder. Restore the sync: `DesignFilesPanel`
reports its `currentDir` via a re-added `onCurrentDirChange` prop, and
`FileWorkspace` feeds it back into `uploadDir`.

Regression in DesignFilesPanel.test.tsx: navigating into a folder reports the
nested dir so the parent creates files there.

(cherry picked from commit 831781141f)

* fix(web): keep empty persisted folders visible in Design Files

#3358's redesign derived the folder list from file paths only and dropped the
`folders` prop, so an empty persisted folder (user-created, or an empty dir in
an imported working directory) vanished — it couldn't be opened or deleted
until a file lived inside it. Thread the fetched `folders` back into
DesignFilesPanel and merge them into `dirsAtCurrentDir`; don't show the
project-empty state when folders exist; and treat a persisted folder as a
valid currentDir so navigating into an empty one doesn't bounce to the root.
FileWorkspace passes `folders={projectFolders}` (already fetched).

Regressions: an empty persisted folder shows at the root, and a nested empty
folder is navigable.

* fix(web): reset project folders when switching projects

With persisted folders now rendered, FileWorkspace kept the previous project's
folder list until the async fetchProjectFolders resolved, so switching to a
different (empty) project briefly showed the old folders and suppressed the
new project's empty state. Clear projectFolders synchronously on projectId
change, before the fetch.

Regression in FileWorkspace.test.tsx: rerendering with a new projectId while
the new folder fetch is pending drops the previous project's folders.

(cherry picked from commit 36e336f312)

---------

Co-authored-by: elihahah666 <template@refly.ai>
Co-authored-by: qiongyu1999 <2694684348@qq.com>

* fix(web): scope app file links to current project

Generated-By: looper 0.9.2 (runner=fixer, agent=codex)

* feat: stream agent questions token-by-token (#3582) [v0.10.0] (#3637)

* feat: stream agent questions token-by-token (#3582)

* feat(web): stream AskUserQuestion and discovery questions token-by-token

Render both agent question surfaces as they generate instead of popping in
only once the whole payload finishes.

- AskUserQuestion (Claude tool): builds on the live tool-input streaming
  landed in #3382. When a still-streaming tool's name is AskUserQuestion,
  render a read-only StreamingAskUserQuestionCard that grows its questions
  and options from the repaired JSON prefix; the interactive (persisted)
  card takes over once the full tool_use lands and the id leaves
  liveToolInput.
- <question-form> discovery (any agent, incl. Codex): the streaming parser
  now repairs the truncated prefix so a question shows the moment its label
  exists and its options grow in one at a time, instead of waiting for each
  question object to fully close. The Questions panel UX (skip countdown,
  auto-continue, reveal) is unchanged.

Shared repairJsonPrefix/parsePartialJson live in runtime/partial-json.ts.
No daemon, contract, or provider changes — this reuses #3382's
tool_input_delta / liveToolInput plumbing.

* fix(web): harden streaming <question-form> parser (review follow-ups)

Two merge-safe follow-ups from review on parsePartialQuestionForm:

- Derive form-level id/title/description from the parsed top-level object
  instead of regex-scanning the whole partial body, so a nested question or
  option id/description can no longer masquerade as the form's own. form.id
  keys the live Questions panel (ProjectView), so a mid-stream identity
  change would remount it. Removes extractJsonStringField.
- Strip a (possibly partial) trailing ``` fence so the live preview keeps
  streaming in the gap between the fenced JSON body closing and the
  </question-form> close tag arriving, instead of dropping back to empty.

Both covered by regression tests in question-form.test.ts.

* fix(web): preserve submitLabel + string-safe fence trim in streaming form parser

Second round of review follow-ups on parsePartialQuestionForm:

- Carry top-level submitLabel through the streaming preview, mirroring
  tryParseForm, so a custom CTA label doesn't flicker in only once the
  close tag arrives.
- Only strip the trailing ``` fence when those backticks are the closing
  wrapper, not content of a JSON string value still being typed — a label
  like "Use ```" mid-stream kept losing its backticks. The trim now checks
  it isn't inside an open JSON string first.

Both covered by regression tests in question-form.test.ts.

* fix(web): honest status badge on WebFetch/WebSearch cards

WebFetchCard / WebSearchCard hard-coded a green check badge regardless of
the real tool state, so a slow or failed fetch/search rendered as
false-green. Thread result / runStreaming / runSucceeded through both and
reuse ResultBadge (as the other tool cards do) so the badge reflects the
actual outcome.

Pre-existing on main (only surfaced in this PR's changed range because the
new streaming card additions shifted these lines down); folded in here
since it's a one-line-per-card fix in a file this PR already touches.

* fix(web): keep partial-JSON repair stable across a dangling string escape

repairJsonPrefix() appended a closing quote even when the stream stopped on
an incomplete escape inside an open string, leaving invalid JSON: `{"a":"x\`
became `{"a":"x\"` (the quote got escaped) and `{"a":"x\u` became an invalid
`\uXXXX`. parsePartialJson() then returned null, collapsing the live
AskUserQuestion / question-form preview to its empty frame mid-stream
whenever a prompt mentioned a Windows path, regex, or escaped quote.

Now neutralize a trailing incomplete escape before closing the string: drop a
partial `\uXXXX` (0-3 hex) and a lone (odd-run) trailing backslash. Completed
escapes and even backslash pairs (e.g. `C:\\Users\\`) are preserved.

Regression tests in ask-user-question.test.ts cover the repair cases and a
path-like prompt that keeps its last visible content.

* fix(web): trim unfinished scalar values in partial-JSON repair

repairJsonPrefix() only trimmed dangling commas / empty `"key":`, so a delta
that stopped mid-scalar left invalid JSON: `{"…","multiSelect":f` closed to
`{…:f}` and failed to parse, collapsing the live preview whenever a boolean,
null, or number token split across deltas. The cleanup loop now also trims a
partial `true`/`false`/`null` and a number cut on `.`/`e`/sign, leaving the
`"key":` / `,` / `[` boundary for the next iteration. Complete literals
(`true`, `12`, `1.5`, `1e3`) and string values that merely start like a
literal are not prefixes of these patterns, so they survive.

Regressions added in both the AUQ and question-form suites (a boolean split
across deltas keeps already-visible questions) plus repair-level unit tests.

* fix(web): stable preview question ids while a form streams

shapeStreamingQuestions surfaced a question as soon as its label existed, but
mapRawQuestion falls back to q${index+1} until the real id streams — so a
question that arrives label-first and gains its canonical id later would
change id mid-stream. The Questions panel stays editable while building and
keys fields / answers off q.id, so that id change remounts the field and
orphans whatever the user already picked. Pin the preview id to the array
index for the whole streaming phase (questions only append once their label
exists, so the index is stable); the final non-preview parse still uses the
real id.

Regression in question-form.test.ts: label-first then id-later keeps id q1.

* fix(web): preview question ids match the final parse (no orphaned answers)

The previous index-pin kept ids stable *within* the preview but still
differed from the final canonical id, so an answer entered while the panel
was building got stranded at the preview→final swap (field re-keys from `q1`
to `platform`, reads nothing). Surface a streaming question only once its
canonical id is determinable instead: use the streamed `id`; or, for an
already-closed (not in-flight) object with no id, the `q${index+1}` fallback
that the final parse will also assign; hold the in-flight object until it
gains an id. Preview id now always equals the final id.

Updated/added regressions in question-form.test.ts: a label-first object is
held until its id is determinable, and preview ids match splitOnQuestionForms.

* fix(web): stable positional keys in the streaming AskUserQuestion card

The streaming card keyed fields on q.question and options on opt.label, but
those strings grow token-by-token, so every delta changed the key and
remounted the subtree — replaying the reveal animation on each token instead
of mounting once. Switch to positional keys (questionIndex /
questionIndex:optionIndex); questions and options only append during
streaming, so the index is stable and each node updates its text in place.

Regression: StreamingAskUserQuestionCard.test.tsx asserts the field/option
DOM nodes are reused (===) across successive grown prefixes.

* fix(web): stream a closed no-id question before the close tag

The previous gate held *every* last no-id question as if in-flight, but a
last object whose braces have already streamed has a final fallback id
(q${index+1}) and should appear immediately — holding it broke the
"question shows the moment its label exists" behavior for forms that omit
explicit ids. Distinguish closed vs still-open via a string-aware
balanced-object count: surface a question once its object has closed (id
final) or it already carries an `id` (stable); keep holding only an
in-flight object with no id yet.

Regressions: a single closed no-id question streams pre-close-tag (id q1),
while the same content with an unclosed object stays held.

* fix(web): suppress hedging fallback text while a live AskUserQuestion streams

suppressAskUserQuestionFallbackText ran on the persisted blocks before the
liveBlocks pass, so during streaming (no persisted tool_use yet) seenAskUser
Question never flipped and any duplicate hedging markdown survived into
persistedBlocks while the live card was appended after — the exact
duplicate-question state the card removes, just earlier in the turn. Seed the
suppressor with whether a live AskUserQuestion is present in liveToolInput so
it fires during the streaming phase too.

Regression in AssistantMessage.test.tsx: fallback prose coexisting with a live
AskUserQuestion in liveToolInput is suppressed, while normal prose without a
live AUQ is kept.

* fix(web): order-aware fallback suppression for live AskUserQuestion

The previous fix seeded the suppressor with "a live AskUserQuestion exists",
which dropped ALL text blocks in the turn — including legitimate preamble
("I need one clarification:") that precedes the tool call, not just duplicate
hedging. Make it order-aware instead: suppress text only after an
AskUserQuestion in block order, and have the suppressor recognize a live
(streaming) AskUserQuestion live-tool block the same way it recognizes a
settled tool-group. Preamble before the card is kept; hedging after it is
dropped — matching the settled-case semantics.

Regression updated in AssistantMessage.test.tsx: intro preamble coexisting
with a live AskUserQuestion in liveToolInput is preserved.

* fix(web): place the live AskUserQuestion at its real stream position

The order-aware pass still appended the live AUQ block after all persisted
blocks, so hedging text the model emits *after* starting the tool call (which
is already in events, hence before the appended block) was kept. Carry the
event index the tool started at (`seq`, recorded in liveToolInput when the
first input_json_delta arrives) and split the events around it: blocks before
`seq` are preamble, the live AUQ block goes next, blocks after `seq` are
hedging. Suppression then keeps the preamble and drops the trailing hedging,
exactly matching the settled tool_use case.

Regression in AssistantMessage.test.tsx: with the tool starting after the
preamble (seq=1), preamble before the card is preserved and hedging after it
is dropped.

* fix(web): account for buffered preamble when pinning live-tool seq

`seq` was read from `latestAssistantMsg.events.length`, but preamble text goes
through `appendTextEvent` and isn't committed to `events` until the next
rAF/250ms flush. When intro prose and the first `tool_input_delta` arrive in
the same burst, that read undercounts by one, so the live AskUserQuestion was
positioned before the preamble and suppression dropped it as hedging. The
buffer now exposes `hasPendingText()`, and seq adds 1 for a still-pending
preamble chunk (it commits as one text event just before the tool position).

Regression in buffered-text-pending.test.tsx: appended text is reported
pending and absent from events until flush, then commits as one event.

* fix(web): re-render the streaming row on live tool-input deltas

AssistantMessage is memoized with a prop allowlist that omitted liveToolInput,
so a burst of pure tool_input_delta traffic (which only changes liveToolInput,
not message) was swallowed by the comparator — the live AskUserQuestion card /
code box wouldn't grow until some unrelated change, usually the final
tool_use. Add liveToolInput to ASSISTANT_MESSAGE_COMPARED_PROPS, and have
ChatPane pass it only to the streaming row (undefined elsewhere) so just that
row re-renders per delta, not all N messages.

Also widens ChatPane's liveToolInput type with the optional seq field.

Regression in AssistantMessage.test.tsx: with the same message object,
changing only liveToolInput re-renders and grows the card prompt.

(cherry picked from commit 6e10fa4852)

* fix(web): keep the streaming question-form id stable (no churn)

`parsePartialQuestionForm` derived the form `id` from the streaming body
`id` token, but that arrives char-by-char and `parsePartialJson` repairs the
open string, so `form.id` churned (`"d"` → `"di"` → …). `id` keys the live,
still-editable Questions panel (ProjectView's questionFormKey, QuestionForm's
data-form-id / input names), so each partial-id change remounted the panel and
discarded in-progress answers. Derive the preview id from the open-tag attr
(complete the instant the tag streams) or the stable default instead; the
final parse (tryParseForm, after the close tag) still adopts the real body id.
This also keeps the earlier nested-id-masquerade guarantee (no body scan).

Follow-up to #3582. Regression: the form id stays `discovery` while the body
id token streams in.

(cherry picked from commit 353e4a64ac)

* fix(web): adopt the question-form body id once complete (no preview→final remount)

Building on the char-churn fix: when a form omits the open-tag id attribute
but carries a body id, the preview used 'discovery' and the final parse
adopted the body id — a single remount at the close tag that could drop
answers. Adopt the body id as soon as its string literal is terminated
(depth-1, nested ids ignored); that value equals what tryParseForm assigns,
so preview id == final id and the panel never remounts on the swap. The id
settles early (it's a leading field), before any question is answerable.

Regressions: a partial body id is not adopted (no churn); a complete body id
is adopted and matches splitOnQuestionForms' final id; nested question ids are
ignored.

(cherry picked from commit 9576f98f56)

* fix(web): reset project folders during render, not in an effect (#3649)

mrcfps flagged that the effect-based folder reset still let the new
DesignFilesPanel (keyed by projectId) mount once with the previous project's
folders before the effect cleared them — briefly suppressing the new project's
empty state, the exact regression this follow-up removes. Move the reset into
the render phase (adjust state during render via a projectId ref, mirroring the
designFilesNav reset), so the new panel never observes stale folders. A stable
EMPTY_PROJECT_FOLDERS constant keeps the render-phase setState idempotent.

Tightens the regression: a wrapper records the folders prop on every
DesignFilesPanel render, and the test asserts no render for the new project
ever sees a non-empty folder list — the pre-paint frame RTL's post-rerender DOM
check can't observe. Red on the effect-based reset, green here.

(cherry picked from commit 3e26d81f4a)

* Fix stale chat anchor spacer on resend

* feat(web): add next-step actions after artifact generation (#3635)

Backports #3439 to release/v0.10.0.

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>

* fix(conversation-forking): fork from the in-memory snapshot so an unpersisted turn no longer silently fails (#3648) (#3656)

"Fork from here" creates a new conversation by asking the daemon to copy the
source conversation's messages up to the fork point, looked up in SQLite by
`forkAfterMessageId`. When the fork point is an assistant turn whose run
errored or had its connection reset before the message was written to the
database (common with AMR/ACP "Connection reset by server"), that id is not in
the DB. The daemon returned 404 "fork message not found", `createConversation`
turned the non-OK response into `null`, and `handleForkFromMessage` threw into
a quiet error state — so no new conversation opened and the user never switched
into the fork. The page just flashed and stayed put.

Make fork copy what the user is actually looking at: the chat "Fork" action now
sends the in-memory messages slice (up to and including the fork point) as
`seedMessages`, and the daemon prefers that snapshot over reading the source
conversation from the DB by id. The Side Chat path (no `seedMessages`) is
unchanged and still seeds from `seedFromConversationId`.

Red spec: apps/daemon/tests/projects-routes.test.ts "forks from a
client-supplied snapshot when the fork point was never persisted" — 404 on
main, 200 on this branch.

* fix(home,daemon): keep mode card on-screen and let plugin authoring pause on a question-form (backport #3640 to v0.10.0) (#3659)

* fix(home,daemon): keep mode card on-screen and let plugin authoring pause on a question-form

Two release/v0.10.0 firefight fixes:

- Home Design Agent description card: the mode toggle's popover card is
  only shown on the home hero and anchors upward, so at its default 360px
  it crowds (and on smaller windows clips) the page header. Cap its height
  on the home hero so it always clears the top chrome and scrolls for the
  overflow.

- Plugin authoring 'ended before generating the required generated-plugin
  artifacts' error: the od-plugin-authoring turn-1 flow emits a
  <question-form> to collect the brief, then stops and waits for the user.
  That turn legitimately closes with code 0 and no artifacts yet, but the
  missing-artifacts guard treated it as a failure. Track whether the agent
  emitted a clarifying <question-form> in its streamed text and skip the
  guard in that case.

Red spec: chat-route.test.ts 'does not fail plugin authoring when the
turn-1 reply is a clarifying question-form awaiting the brief' (red on
main, green on branch).

(cherry picked from commit 1eee8b71ce)

* fix(daemon): recognize <ask-question> alias in plugin-authoring clarification guard

The web form parser (apps/web/src/artifacts/question-form.ts) accepts both
<question-form> and its <ask-question> alias as a valid brief form. The
plugin-authoring missing-artifacts guard only matched the canonical tag, so
a turn-1 reply using the alias still rendered a valid form in the UI but
fell through to the 'ended before generating…' failure. Mirror the parser's
accepted tag set in the daemon guard and add a regression test for the
alias path.

Addresses Looper review feedback on #3640.

(cherry picked from commit 80033dcdbe)

* fix(daemon): gate plugin-authoring clarification carve-out on a renderable form body

The previous carve-out matched only the question-form/ask-question opening
tag, so a malformed (non-JSON) body — which the web parser keeps as raw
prose with no rendered form card — would still suppress the missing-artifacts
failure, turning a hard failure into a false success.

Mirror the parser's minimal renderable contract in a daemon-local helper
(emittedRenderableQuestionForm): a closed question-form/ask-question block
whose body parses as JSON to an object with a non-empty questions array.
Buffer the run's visible text and validate at close. Documented as a
deliberate mirror of apps/web/src/artifacts/question-form.ts since the app
boundary forbids importing it; promote to packages/contracts if they drift.

Tests: question-form and ask-question fixtures now use valid JSON bodies,
plus a new regression pinning that a non-renderable (non-JSON) form body
still fails. Addresses Looper review feedback on #3640.

(cherry picked from commit ba28f07edd)

* fix(daemon): keep question-form close-tag scan Unicode-safe in the daemon mirror

The mirrored renderable-form check lowercased the whole buffer before
indexing the close tag. Some code points expand under toLowerCase ('İ' ->
'i̇'), shifting every offset after them, so closeIdx desynced from openEnd
(original-string coordinate) and the JSON body slice was corrupted —
failing a valid form when a casing-expanding char preceded it.

Mirror the web parser's findCloseTag: scan char-by-char and lowercase each
fixed-length candidate slice, returning an index in the original-string
space. Added a daemon regression with an 'İ' preamble before a valid form.

Addresses Looper review feedback on #3640.

(cherry picked from commit 3207d1dc2b)

* fix(web): keep question forms in questions panel (#3661)

* feat(web): collapsible entry rail + quiet new-user templates + icon-only topbar chips (#3617) (#3666)

* feat(web): collapsible entry rail + quiet new-user templates + icon-only topbar chips

Rework the entry view to keep new users focused on the hero composer:

- Nav rail collapses to a single topbar toggle by default and docks as an
  overlay on demand; a hover-reveal collapse button on the logo folds it back.
- For users with no projects, the Community templates gallery starts hidden
  behind a faint bottom hint; an upward scroll gesture or click smoothly
  reveals it, with a pinned collapse pill to fold it away again.
- Collapse the Discord / model-switcher / Use-everywhere topbar chips to
  icon-only with hover tooltips; the GitHub Star chip stays a full pill.

* test(e2e): expand collapsed nav rail before driving rail destinations

The entry nav rail now starts collapsed, so the UI smoke tests that click
`entry-nav-*` items (or assert the rail/logo is visible) timed out. Add an
idempotent `ensureRailOpen` helper and call it before rail nav interactions
across the e2e/ui suite; adjust the two rail-structure assertions to expand
first.

* test(e2e): reach home via Home nav and expand rail for help launcher

The logo now doubles as a hover-to-collapse control, so clicking it to return
home is intercepted by the collapse overlay — route via the explicit Home nav
item instead. Also expand the rail before opening the rail-footer help menu,
and guard the execution-pill nav loop with ensureRailOpen.

* fix(web): make collapsed rail inert and restore Discord chip title

Address review feedback:
- The collapsed nav rail kept its logo + nav buttons mounted and focusable, so
  a fresh Tab on the home view landed on invisible rail controls before the
  visible toggle/hero. Mark the rail `inert` while collapsed so its descendants
  leave the keyboard tab order and pointer flow; add a keyboard regression test.
- Restore the Discord chip's native `title` alongside `data-tooltip` so the
  existing topbar contract test (entry-topbar.test.ts) keeps passing.

* fix(web): make collapsed templates gallery inert and keep rail foldable on touch

Address second review pass:
- The collapsed new-user templates gallery was only aria-hidden + height-clipped,
  so its still-mounted buttons/tabs/search stayed in the keyboard tab order.
  Mark the gallery body `inert` until revealed (deps include `enabled` so it
  applies once the async-mounted body appears); add a keyboard regression test.
- On coarse-pointer / non-hover devices the hover-only collapse button plus the
  display:none topbar toggle could strand the rail open. Keep the collapse
  control visible + tappable under `@media (hover: none)`; add a CDP-emulated
  touch regression test.

* fix(web): keep inline switcher chip accessible name in icon-only topbar

---------

Co-authored-by: elihahah666 <template@refly.ai>
Co-authored-by: qiongyu1999 <2694684348@qq.com>

* fix(web): stop early analytics events shipping app_version=0.0.0 (#3609) (#3614)

useAppVersion() reads the daemon-pinned version from /api/version at
runtime, but PostHog's app_version rode solely on the register()
super-property, which races that fetch. High-frequency early events
(page_view above all) captured before the version resolved shipped with
app_version='0.0.0' permanently — the dashboards showed 0.0.0 on every
build after the runtime-fetch approach landed.

resolveAppVersionForCapture() awaits a shared single-flight /api/version
fetch before every capture path (client init, super-property register,
track, consent re-init) and stamps app_version/ui_version onto each event
explicitly, so no event ships with the placeholder once the version is
knowable. The hook still returns the placeholder for first paint.

Red spec: tests/analytics-app-version.test.tsx gains
resolveAppVersionForCapture cases (resolves real version before capture;
no refetch when already real).

* feat(composer): reconcile context with prompt + per-connector brand color (#3625) (#3670)

* feat(composer): reconcile context with prompt + per-connector brand color

Two increments on top of the Lexical composer (#3516):

- Filter contextPlugins/contextMcpServers/contextConnectors at submit time
  by whether the entity's @mention still survives in the prompt text. When a
  user deletes a mention pill, that plugin/MCP/connector is no longer sent to
  the agent. Adds mentionTokenPresent() to inlineMentions.

- Give connector mention pills a per-connector brand color (Notion black,
  Figma purple, etc.) by setting an inline --m-hue in MentionNode.createDOM,
  reusing the existing CSS hue machinery instead of the shared connector green.
  Adds a curated brand-color map with a hashed fallback.

* fix(composer): keep mention context when punctuation trails the pill

mentionTokenPresent() reused the parser's strict isMentionRightBoundary(),
so a still-visible atomic pill serialized as `@Slack,` was treated as
deleted and dropped from contextPlugins/contextMcpServers/contextConnectors
at submit time. Add a submit-time boundary that also accepts trailing
clause/sentence punctuation, leaving the parser invariant untouched, plus a
HomeView regression for the punctuation case.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* fix(composer): make connector pill brand hue theme-aware

Near-black curated brand colors (Notion, GitHub, Slack) rendered the
connector mention pill text as dark-on-dark in the app's dark theme,
since chat.css derives the text as color-mix(--m-hue 72%, --text) and
--text is light in dark mode. connectorBrandColor now takes the live
theme and lightens hues below a luminance floor so they stay legible,
while bright brands (Figma, Linear) pass through unchanged.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* fix(composer): make connector pill hue follow live theme switches

Recompute a mounted connector pill's brand --m-hue when <html data-theme>
or the OS prefers-color-scheme changes, not only on node create/update, so
a pill inserted in light mode no longer keeps its near-black hue (and
dark-on-dark text) after a live switch to dark mode.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* feat(composer): collapse footer tools into one "+" menu; drop duplicate context badges

- Replace the separate "@" context-trigger and paperclip attach buttons with a
  single "+" button. Its popup offers Attach files plus Connectors/Plugins/MCP
  right-side flyout submenus (Plugins/MCP searchable + scrollable). Picking an
  entry inserts the inline @mention pill via the existing pickX handlers; "Add …"
  rows route to the plugins / integrations views.
- Stop rendering the top plugin/connector/MCP context-badge row — those now live
  as inline mention pills in the composer, so the badges were duplicates. Staged
  file chips still render there. showActiveContextRow no longer keys off the
  selected*Contexts arrays.
- New i18n keys (addMenu/addPlugin/addConnectors/addMcp/noPlugins/noMcp/
  noConnectors) in types.ts + en + zh-CN; other locales inherit via ...en.

* fix(composer): repair collapsed-menu test selector and complete addMenu i18n

Update the HomeView context-picker test to open the plus menu before
asserting the attach affordance, since the collapsed footer now renders
home-hero-plus-attach only after home-hero-plus-trigger is clicked.

Add the seven home-hero add-menu keys (addMenu, addPlugin, addConnectors,
addMcp, noPlugins, noMcp, noConnectors) to every locale beyond en/zh-CN so
the i18n contract holds across all 18 locale files.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* test(web): drop assertions on removed context-badge row

The PR removed the duplicate plugin/connector/MCP context-badge row
(context now renders as inline @mention pills), but four HomeView tests
still asserted the dropped `home-hero-context-plugin-*` testids and went
red. Re-anchor them to behavior that survives: the submit payload's
`contextPlugins`/prompt-pill text for picker flows, and the active
context row's resolved-count label for empty-prompt "Use" handoffs.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* test(web): align entry-chrome e2e with collapsed footer menu

The footer-collapse change removed the always-visible home-hero-attach
control and the plugin/MCP/connector context-badge row, but the entry
chrome Playwright suite still asserted those removed surfaces, failing
Browser tests (and the Validate workspace aggregate).

- Assert home-hero-plus-trigger instead of the removed home-hero-attach.
- Drop the home-hero-context-plugin-* badge assertions; plugin context
  no longer renders a visible badge. Coverage is preserved by the
  existing prompt-text and submit-payload (pluginId) assertions.

* fix(composer): exclude inline-mentioned contexts from active row; reset plus-menu query per submenu

Gate the Home active context row on contexts that have no inline @mention
representation, so selecting only an inline-mentioned connector no longer mounts
an empty row above the editor. Reset the shared plus-menu search query when the
active submenu changes so a stale plugin search no longer filters the MCP list
into a false "No MCP servers" empty state.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* fix(composer): keep context-only Use selections in submit payload

The submit-time reconciliation dropped every selected context that lacked an
inline @mention token, which silently discarded contexts staged through the
plain Use action (it never writes a token). The agent then received no
contextPlugins even though the user explicitly chose the plugin as context.

Tag each selection with inlineBacked: contexts inserted as @mention pills are
only sent while their token survives in the prompt; context-only Use selections
stay in the payload until explicitly cleared. The active context row likewise
counts only context-only selections, so an inline-mentioned-only pick no longer
mounts an empty row. Adds a regression covering the direct Use submit path.

* fix(composer): render removable chips for context-only Use selections

Context-only selections (staged via the plain Use action, no inline @mention
pill) are kept in the submit payload, but the home composer no longer rendered
any chip for them, so they were invisible and unremovable — a kept context
would silently drift into later freeform prompts.

Render a removable chip in the active context row for each context-only
plugin/MCP/connector selection, wired to the existing remove handlers. Adds a
regression that stages a plain Use context, clears it via the chip, and asserts
the submit payload no longer includes it.

---------

Co-authored-by: elihahah666 <template@refly.ai>
Co-authored-by: qiongyu1999 <2694684348@qq.com>

* fix(web): stop conversation list flashing "0 msg" while switching chats (#3663) (#3672)

* fix(web): stop conversation list flashing "0 msg" while switching chats

The chat tab's conversation list derives the active conversation's message
count from the live `messages` array. Switching conversations resets that
array to [] while the new conversation's history is fetched, so the active
row rendered a phantom "0 msg" for the duration of the load (and stayed at 0
if the load failed) instead of its real count.

Trust `messages.length` for the active conversation only once the live array
actually reflects it (`messagesConversationId === activeConversationId`);
otherwise fall back to the persisted server `messageCount`. The live count is
still used during streaming so the active row stays fresh as turns arrive.

* fix(web): wire messagesConversationId for the other ChatPane mounts

SideChatTab and DesignSystemFlow also render the conversation list but did not
pass the new marker, so their active row fell back to the persisted count and
lost live-count freshness. Both own a single loaded conversation whose messages
always reflect the active id, so pass that id through to keep the active row's
live count authoritative (restores pre-change behavior). ProjectView still
passes its real messagesConversationId (null during the switch/load window).

* fix(web): keep secondary ChatPane mounts on the persisted message count

The SideChatTab and DesignSystemFlow mounts both have loader windows where the
live `messages` array does not reflect the active conversation —
useConversationChat resets to [] before a load, and DesignSystemFlow's loader
does not retag projectChatMessages on switch. Marking the live array as
authoritative there would reintroduce the same phantom/stale count this PR
removes. Omit messagesConversationId for those mounts so their list uses the
stable persisted conversation.messageCount; only ProjectView, which tracks the
marker precisely, opts into the live count.

* fix(web): bottom-anchor the chat transcript so short and forked conversations don't strand at the top (#3669) (#3673)

A short conversation — most visibly a fresh "Fork from here" branch — rendered its
messages stuck to the top of the chat log with a large empty area below, instead of
resting against the composer like a normal chat. Two causes:

1. `.chat-log` is a flex column with no vertical anchoring, so when the content is
   shorter than the viewport the messages sit at the top (~530px of dead space below).
2. The anchor-to-top reserve (`.chat-log-tail-spacer`, sized while a reply streams so
   the user's turn can reach the top) never collapsed once the turn finished. A short
   completed turn was left pinned at the top above ~540px of reserved blank.

Fix:
- Bottom-anchor the transcript with `margin-top: auto` on the first row. When content
  is shorter than the viewport the auto margin absorbs the slack and the messages rest
  against the composer; when it outgrows the viewport the margin resolves to 0 and the
  log scrolls normally with the top fully reachable. (Deliberately not
  `justify-content: flex-end`, which clips overflowing content above the scroll range.)
- Collapse the tail spacer when streaming ends, and reset it (plus the anchor flags) on
  conversation switch, so a finished or freshly opened short conversation settles cleanly
  instead of inheriting a stale reserve. Anchor-to-top during an active turn is unchanged.

Verified live (Playwright, local daemon+web): fresh short conversation last-message-to-
bottom gap 532px -> 20px; completed short turn tail spacer 549px -> 0; long conversation
still scrolls and reaches the top (scrollTop 0, first row at +18px).

* fix(web): flip deck preview to a queued send's marked slide (#3668) (#3681)

* fix(web): flip deck preview to a queued send's marked slide

When several edits are queued against marked elements on different deck
slides, the preview stayed parked on slide 1 while the agent drained the
queue — the user couldn't see each edit land in context.

The mark's slide index was already captured into the comment snapshot and
target, but it was dropped when the snapshot became a ChatCommentAttachment,
so nothing downstream knew which slide a queued send touched. Carry
`slideIndex` through the contract and both attachment builders, then have
ProjectView arm a `slideNavRequest` (mirroring `shareRequest`) the moment a
queued send starts processing. FileWorkspace activates the deck tab if it is
open and FileViewer consumes the nonce once to flip the preview via the
existing `od:slide` go bridge, writing the cached slide state first so a
freshly-mounted iframe restores to the same slide on load.

No-op for plain prompts, free pins, or marks without a slide index.

* fix(web): keep deck slide-nav consume-once across viewer remounts

The consumed-nonce guard lived in a per-mount ref, so leaving the deck tab
and returning remounted HtmlViewer, reset the ref, and replayed the still-live
slideNavRequest — yanking the preview back to the queued slide and clobbering
manual navigation. Move the guard into shouldConsumeSlideNav, keyed by preview
state key outside the component, so "consume once" survives remounts. Add a
unit spec covering first-handling, fresh-nonce, and per-file isolation.

* fix(web): only flip a deck whose tab was open when the queued send started

slideNavRequest stayed live in parent state, so a request for a deck that was
closed at processing-start would resurface and jump the preview when the user
opened that file by hand much later — not the follow-along behavior the feature
promises. Decide deliverability once, at fire time: FileWorkspace marks the
nonce deliverable only when the target deck is already an open tab, and the
viewer is fed the request only for that deliverable nonce on the active file.
A closed deck never becomes deliverable, so opening it later cannot jump.

Pure helpers isSlideNavDeliverableNow / deliverableSlideNavForActiveFile carry
the decision, with specs for the closed-deck-opened-later case.

* Fix URL preview image export capture

* fix(web): keep generation progress card on the design-files tab (#3671) (#3680)

* fix(web): keep generation progress card on the design-files tab (#3671)

The right-side preview pane showed the empty "Creations will appear
here" file list while an agent was actively generating, instead of the
generation progress card with its understand/generate/prepare steps.

PR #3516 (Lexical composer rewrite) added an `activeTab !==
DESIGN_FILES_TAB` clause to `showGenerationPreview`. The design-files
tab is the default landing tab, so on a fresh project the in-flight
generation preview was suppressed and the idle empty state won the
surface. Before #3516 the preview branch rendered ahead of the
design-files branch with no tab guard.

Drop the clause so the progress card again takes priority over the empty
file list. `buildGenerationPreviewState` returns null once a previewable
artifact exists, so a finished artifact is never hidden behind the card.

Adds a red-spec FileWorkspace test that renders a generating run on the
design-files tab and asserts the generation-preview stage is shown.

* fix(web): scope generation-preview override to the empty design-files tab

Address review: dropping the DESIGN_FILES_TAB guard outright let the
generation card win for every in-flight run on the design-files tab,
hiding the file browser of a populated project until a previewable
artifact existed. Scope the override to the truly empty tab, mirroring
DesignFilesPanel's own empty-state gate (no files, no live artifacts, no
folders), so a populated project keeps browsing while generating.

Adds a regression test that a populated design-files tab still renders
file rows while a run is active.

* feat(web): simplify Home composer (drop plugin-inputs form & example pill, move Design Agent next to Send) (#3645) (#3683)

* feat(web): simplify Home composer (drop plugin-inputs form & example pill, move Design Agent next to Send)

- Remove the inline plugin-inputs form (ARTIFACT KIND / AUDIENCE / TEMPLATE) from the Home composer; required inputs no longer gate Send (defaults still flow to the backend).
- Remove the dismissible "selected example" pill above the editor; picking an example still seeds the prompt.
- Move the Design Agent toggle from the footer-left group to sit directly left of the Send button.

* fix(web): keep client-side required-input gate on Home composer

Dropping the inline plugin-inputs form left pluginInputsAreValid() always
returning true, so a required field with no default that the user blanks out in
an editable Home scenario kept Send enabled and only failed at click-time apply
with a generic "Failed to apply …" error (the daemon's validateInputs rejects
missing required inputs).

Restore a client-side gate that mirrors the daemon's required-input rule and
names the missing field in the submit error, so these Home flows fail fast.
Extracts the pure check into utils/pluginRequiredInputs with unit coverage.

* fix(web): drop example from active-row mount condition

With the example pill removed from the Home composer active row, keeping
selectedPromptExample in showActiveContextRow mounted an empty .home-hero__active
row when an example was picked with no other active context. Remove it from the
condition so the row only renders when it has visible content.

---------

Co-authored-by: elihahah666 <template@refly.ai>
Co-authored-by: qiongyu1999 <2694684348@qq.com>

* ci(release): throttle the per-push nightly build instead of debouncing it (#3682) (#3687)

`notify-release-feishu` builds a full nightly (via release-stable, channel=nightly)
and posts a Feishu card on every push to `release/**`. Its concurrency used
`cancel-in-progress: true`, so a newer push cancelled the in-flight build. When a
release branch is busy — pushes spaced closer than a full cross-platform build
takes — every build was cancelled by the next push and no nightly was ever cut.
(Observed on release/v0.10.0: a run of pushes all cancelled, the last successful
nightly was hours old.)

Switch to `cancel-in-progress: false` (throttle): the running build finishes, and
pushes that arrive during it coalesce — GitHub keeps the in-progress run plus only
the newest pending run, cancelling older pending ones. A busy release branch now
always cuts a nightly and converges on the latest commit, with at most one build
running plus one queued. Trade-off: a newer push no longer interrupts a running
build for an older commit; it waits one build cycle. For nightly that's the right
call.

* feat(desktop): play white brand splash video on startup (#3674) (#3689)

* feat(desktop): play white brand splash video on startup

Replace the dark "Waiting for the web runtime URL…" placeholder with the
brand intro animation on a white background. The clip plays once and then
freezes on the settled "Open design" logo. The window only swaps to the web
runtime after the clip has had time to play through (MIN_SPLASH_MS), so it
never flips mid-animation, and it holds the final frame when the runtime
takes longer to boot.

The clip is embedded as a base64 data URL so it renders identically in dev
and in packaged builds without any electron-builder asset wiring — the
packaged app exposes no reliable on-disk asset path from the desktop main
bundle (the previous icon lookup already silently fell back to null there).

* feat(desktop): show splash in parallel with boot, reveal on web mount

Make the white brand splash a dedicated window so it can do real work:

- Packaged cold start now creates the splash BEFORE awaiting the daemon/web
  sidecars, so the animation is on screen during the whole boot instead of the
  user staring at no window at all for a few seconds. The 2s minimum splash now
  overlaps startup rather than being added on top of it.
- The main window starts hidden and loads the web app behind the splash. We
  reveal it only once the web bundle reports it has actually mounted
  (data-od-app-mounted="1") and the minimum splash time has elapsed, then close
  the splash. This removes the dark "Loading Open Design…" web shell that used
  to flash between the splash and the app. A hard timeout guarantees the user is
  never stranded on the splash if the mount signal never arrives.

tools-dev/dev keeps working unchanged: when no splash window is handed in, the
runtime creates its own.

* fix(desktop): measure splash hold from window creation, not adoption

PerishCode review: in packaged startup the splash window is created before
`await startPackagedSidecars(...)`, but the minimum-hold clock was stamped later
inside `createDesktopRuntime` (after the sidecars had finished booting). The
reveal then waited `MIN_SPLASH_MS` from that post-boot instant, so a fast web
mount still added almost another 2s on top of boot — re-introducing the very
delay the parallel splash was meant to remove.

`createSplashWindow()` now returns a `{ window, startedAt }` handle stamped at
creation; the packaged entry forwards `startedAt` via `splashStartedAt` and the
runtime measures the hold from there. Runtime-created dev splashes stamp their
own creation time, so the hold overlaps boot in every path.

* fix(desktop): gate DesktopRuntime.show() behind the splash reveal

PerishCode review: the main window now starts hidden and is revealed only after
the web app mounts, but `DesktopRuntime.show()` still called `window.show()`
unconditionally. The packaged entry routes a pending second-instance focus
through that method via `onDesktopReady`, so launching the app twice while the
sidecars/web are still booting would surface the half-loaded main window and
bypass the reveal gate — reintroducing the startup flash.

`show()` now focuses the splash while the runtime is pre-reveal and only shows
the main window once `revealWhenReady()` has fired.

* fix deck keyboard pagination double step

* Fix official plugin and skill catalog search

* fix(web): clear plugin context after send

* fix(web): gate bundled plugin provenance in catalog

Generated-By: looper 0.9.3 (runner=fixer, agent=codex)

---------

Co-authored-by: yinjialu <2481044901@qq.com>
Co-authored-by: Ray Xi <109605599+Siri-Ray@users.noreply.github.com>
Co-authored-by: Amy <1184569493@qq.com>
Co-authored-by: elihahah666 <template@refly.ai>
Co-authored-by: qiongyu1999 <2694684348@qq.com>
Co-authored-by: Amy <58060647+AmyShang-alt@users.noreply.github.com>
Co-authored-by: chaoxiaoche <fanzhen910412@gmail.com>
Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: lefarcen <ontf116@gmail.com>
2026-06-05 06:41:27 +00:00
a019bcd5cc fix(web): backport release-only fixes to main ahead of v0.10.0 re-cut (batch 1) (#3711)
* fix(web): avoid duplicate style picker group keys

(cherry picked from commit efbe579df7)

* Fix Design Files and sidebar page layout regressions (#3629)

Fixes two web UI layout regressions:

- Prevent Design Files name cells from collapsing to near-zero width, which caused generated file and folder names to wrap vertically while leaving empty space to the right.
- Hide inactive entry views with display: none so Plugins, Integrations, Design Systems, and Automations no longer reserve hidden Home view height above their page content.

Validated with web typecheck, focused web component tests, pnpm guard, and local visual checks.

(cherry picked from commit 686a0b3702)

* fix(web): preserve failed retry attempts

(cherry picked from commit 0ef1400a6e)

* Auto-open produced HTML previews (#3636)

(cherry picked from commit 1fada1ec6d)

* Fix URL preview image export capture

(cherry picked from commit 4659926be8)

---------

Co-authored-by: yinjialu <2481044901@qq.com>
Co-authored-by: Ray Xi <109605599+Siri-Ray@users.noreply.github.com>
2026-06-05 05:29:32 +00:00
AmyandGitHub b9d0f7e966 ci: add Feishu main alert smoke test (#3704) 2026-06-05 04:23:29 +00:00
9a25aae339 feat(landing-page): add Solution hub at /solutions/ and consolidate Role pages (#3700)
* refactor(landing-page): move Solution → Role pages from /for/ to /roles/

The Solution → Roles pages shipped at /for/<role>/ in #3658. The /for/
prefix reads as an incomplete URL fragment on its own; /roles/<role>/
is a clearer, self-describing path for the audience landing pages.

Rename both the default-locale and [locale] page directories, update
the header dropdown hrefs and the route docstrings to match. R2 hero
image keys stay under landing/assets/for/<role>/ (the bucket key is not
user-visible and the assets already exist there), so only the public
route changes. Old /for/<role>/ URLs are dropped, not redirected — the
pages are hours old with negligible inbound links.

* feat(landing-page): add Solution hub at /solutions/ and fold Role pages in

Builds on the Solution → Role pages from #3658. Two changes to the
Solution information architecture:

1. The Solution nav trigger pointed at /compare/ (the competitor
   comparison hub), which is unrelated to the dropdown's contents. It now
   points at a new /solutions/ overview page that indexes every Solution
   sub-page, grouped exactly like the dropdown: Use cases (what you're
   making) and Roles (who you are). Each card reuses the sub-page's own
   breadcrumb + description from getSolutionPageCopy, so the hub never
   drifts from the pages it links to. The page's own chrome (label,
   heading, one-line lead) lives in a new solutions-index-i18n module,
   typed as a Record over every landing locale so a missing translation
   is a typecheck error; all 18 locales are populated.

2. The Role pages, which shipped at /roles/<role>/, move under
   /solutions/<role>/ so they sit alongside the use-case pages in one
   coherent /solutions/ namespace instead of a parallel /roles/ tree.
   Use-case and role slugs don't collide. Dropdown hrefs and route
   docstrings updated to match. The interim /roles/ paths are dropped,
   not redirected (hours old, negligible inbound links).

Also: the brand logo's default href was '#top', leaving '/#top' in the
URL on the homepage and pointing at a non-existent anchor. It now
defaults to '/' so clicking the logo always returns to the localized
home, matching what every sub-page caller already passed.

Verified: landing-page typecheck (0 errors) and build:static — output
has /solutions/ + all 11 sub-pages across 18 locales, the /solutions/
index, no /roles/ or /for/ routes remain, and hero images still resolve
to their existing R2 keys.

---------

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-06-05 04:09:30 +00:00
f1b59b67c6 feat(web): streamline Home/project composer chrome and pin the Home tab (#3692)
* feat(web): streamline Home/project composer chrome and pin the Home tab

Reduce composer clutter and align the Home and project chat surfaces:

- Drop the artifact-specific footer settings (fidelity/ratio/notes/etc.)
  from the Home composer; the agent now asks for them via the existing
  discovery flow, so the footer keeps only the design-system picker.
- Default the design-system picker to "Auto"; only a user-owned system
  pre-selects.
- Move the CLI (model switcher) into the top-right header next to Discord;
  unify the composer footer buttons to pill shape.
- Extract a shared ComposerPlusMenu (attach / connectors / plugins / MCP,
  with an extra design-toolbox flyout in the project composer) used by both
  Home and the project chat composer; remove the project composer's sliders
  and @ buttons (mentions still trigger by typing @).
- Rename the session-mode label to "Design" (full "Design Agent" copy stays
  in the hover card).
- Pin the Home workspace tab to the leftmost slot, make it non-closable, and
  tighten tab widths.
- Fix the truncated Discord online count in the icon-only top bar; ellipsize
  long plugin/MCP names in the add menu.

* fix(web): address composer + menu review regressions and update tests

- ComposerPlusMenu: reset the shared search query when the active submenu
  changes, so a plugin search no longer cross-filters the MCP flyout
  (restores the old HomeHero behavior).
- ChatComposer: opening the "+" menu latches composerEngaged again, so the
  Plugins / Connectors / MCP submenus aren't empty on a cold composer.
- Keep media-surface footer options (model / ratio / resolution / duration /
  audio type); only prototype/deck fidelity/slide-count/speaker-notes were
  meant to leave the footer.
- Update/trim tests for the removed sliders + tools popover, the @ button, the
  Import-menu design-system switcher, and the prototype/deck artifact footer
  controls; rewire the design-toolbox tests through the new "+" menu; assert
  the Home tab is pinned and non-closable.

* fix(web): guard composer + menu picker mousedown to preserve caret

The shared ComposerPlusMenu connector/plugin/MCP picker rows ran onPick*
handlers (which call insertMention) without cancelling mousedown, so the
button stole focus first and Lexical fell back to selectEnd(), inserting
mentions at the end of the draft instead of the caret. Add the
preventDefault guard and port the deleted caret-regression coverage to the
shared menu path.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* fix(web): defer prototype/deck artifact inputs and protect + menu caret

Address the two blocking review findings on PR #3692:

- HomeView.submit() now strips fidelity / slideCount / speakerNotes from the
  submitted plugin inputs via stripArtifactFooterInputs(), so removing the
  footer actually defers those questions to first-turn discovery instead of
  forwarding the plugin manifest defaults. The prose brief still renders them.
- ComposerPlusMenu pick rows (connector / plugin / MCP) cancel mousedown so the
  editor keeps focus and the caller's insertMention lands at the caret, not the
  draft end — restoring the #3195 contract that the deleted tools-menu-caret
  test used to cover.
- Add ComposerPlusMenu.test.tsx covering the mousedown-preventDefault contract
  and the submenu query reset; update prefill expectations to assert the
  artifact fields are no longer in the apply payload.

* fix(web): keep Home tab pinned leftmost on tab drop

Re-normalize reordered tabs and coerce 'before Home' drop edges to
'after Home' so dragging a project tab onto Home's left edge can no
longer persist a tab ahead of the permanent Home tab. Updates the drag
spec to assert Home stays first after the drop.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* fix(web): always mint a pinned Home tab during tab normalization

normalizeTabsState only reordered an existing Home tab, so users who
closed/replaced Home before this feature shipped could restore a saved
[project, ...] workspace and stay Home-less until manually navigating
home. Prepend a Home tab whenever none survives normalization so the
"Home always exists and is leftmost" invariant also holds for migrated
state. Adds a regression spec booting from saved tabs with no Home entry
and updates two specs that encoded the old Home-less behavior.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-06-05 04:06:46 +00:00
PerishFireandGitHub 0ce9a19bce Add CI gate workflow placeholders (#3701) 2026-06-05 03:29:25 +00:00
8f6cdaaba1 Harden release-beta-s self-hosted beta lane (#3641)
* Fix Windows smoke launcher payload layout

* Harden release-beta-s runner probe

* Use cmd shell for release-beta-s probe

* Add win beta build script for self-hosted lane

* Keep release-beta-s checkout lightweight

* Use explicit local beta version for release-beta-s

* Install dependencies in beta build script

* Repair Electron dist before Windows beta build

* Add release-beta-s timings and diagnostics

* Polish release-beta-s observability guardrails

* Trace tools-pack cache materialization

* Reuse materialized workspace build cache

* Trace Windows packaging segments

* Trace Windows archive process details

* Reuse installer unpacked materialization

* Preserve reusable Windows unpacked projection

* Add fast smoke telemetry for self-hosted beta

* Cache Windows archive outputs for beta lane

* Tighten fast Windows smoke lane

* Scope Windows beta artifact reporting by target

* Clean stale Windows target artifacts

* Wire Windows Authenticode signing

* Expose self-hosted beta validation modes

* Add core Windows smoke profile

* Decouple core smoke from updater fixture version

* Trace Windows smoke lifecycle steps

* Skip unpacked materialization on NSIS cache hit

* Add Nexu S3 beta publisher

* Harden self-hosted Windows beta lane

* Use Windows PowerShell for publish probe

* Run publish probe via repo script

* Require explicit self-hosted S3 public origin

* Run real installer acceptance for external feeds

* Skip ready-prompt checks for external feeds

* Relax optional self-hosted update inputs

* Tolerate BOM in self-hosted publish probe

* Converge self-hosted beta metadata and signing flow

* Run self-hosted beta metadata prep on the runner lane

* Use Windows PowerShell for self-hosted beta metadata checks

* Bypass execution policy in self-hosted beta metadata checks

* Split self-hosted beta metadata publish

* Suppress mc output in beta publish helpers

* Split Windows NSIS payload cache layers

* Require fresh tool dist metadata

* Preserve Windows overlay payload paths

* Default beta-s gate to core

* Optimize win beta packaging reuse

* simplify windows full smoke lifecycle

* preserve onboarding state after windows upgrade smoke

* fix windows tools-pack test portability

* recover stale tools-pack locks

* Allow auto signing fallback for beta builds

* Add self-hosted beta mac arm64 lane

* Quote self-hosted signing choices

* Run self-hosted beta metadata on mac lane

* Use shallow metadata checkout on self-hosted beta

* Use sparse beta metadata checkout

* Make mac release profile bash-compatible

* Allow file sparse metadata checkout

* Isolate self-hosted metadata checkout

* Clear sparse skip-worktree before mac build

* Isolate self-hosted mac build checkout

* Run self-hosted beta publish without aws cli

* Use unsigned beta prefix for mixed platform publish

* Sparse checkout self-hosted beta publish

* Publish mac beta platform before metadata

* Pass mac publish release profile

* Probe mac signing keychain import

* Expand mac signing preflight variants

* Use sudo keychain helper for self-hosted mac signing

* Route self-hosted mac codesign through sudo wrapper

* Normalize mac signing identity name

* Harden self-hosted mac notarization

* Optimize self-hosted mac release cache

* Fix mac signing helper cleanup

* Retry transient mac notarization uploads

* Keep mac notarization fail-fast

* Reduce mac symlink diagnostics noise

* Normalize self-hosted beta platform inputs

* chore: bump beta base version to 0.9.1

* chore: tune release tools-pack cache retention

* test: add temporary cache validation marker

* chore: key windows nsis base payload by content

* Revert "test: add temporary cache validation marker"

This reverts commit e8487bfd3e.

* chore: probe windows nsis payload cache before materializing

* chore: align self-hosted beta smoke modes

* chore: skip mac smoke report upload when disabled

* chore: default self-hosted beta to publish both platforms

* chore: make beta signing modes explicit

* Add release report artifacts and metatool metadata

* Fix Windows release report zip publishing

* Remove artifact handoff from release beta self-hosted lane

* Fix release beta merge follow-ups

* Fix release beta install regressions

* Restore web build dependencies

* Route Windows beta downloads through mirrors

* Use PowerShell 7 for Windows beta lane

* Fix release beta preflight gates

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Fix Windows beta smoke update reuse

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Fix release beta review follow-ups

* Refresh Nix pnpm dependency hashes

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Fix Windows signtool fallback resolution

* Reject stale beta platform manifests

* Validate beta platform manifest run identity

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Validate beta manifest commit identity

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Allow beta metadata publish reruns

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Build tools-pack fully in beta Windows bootstrap

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Emit r2 metadata for Windows beta publish

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Stabilize beta publish browser test

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Stabilize Windows beta metadata smoke test

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Restore multi-platform Vela CLI package

* Refresh Nix pnpm dependency hashes

Generated-By: looper 0.9.5 (runner=fixer, agent=codex)

---------

Co-authored-by: Looper <looper@noreply.github.com>
2026-06-05 03:00:55 +00:00
netteeandGitHub 1467a389a3 fix(github): tighten bug report preflight (#3688)
Generated-By: looper 0.9.3 (runner=worker, agent=codex)
2026-06-05 02:27:49 +00:00
d0d9c02763 feat(web): Manus-style second-level category rail on Home composer (#3686)
* feat(web): add Manus-style second-level category rail to Home composer

Picking Prototype or Slide deck on Home now reveals a compact sub-category
pill row (All + the Community facet sub-categories), filtering the example
preset cards in place without binding a plugin or stamping a badge. Reuses
the existing plugins-home facet taxonomy so the labels match the Community
grid exactly; Landing / marketing is floated first for prototypes.

Also: reorder the first-level rail (Prototype, Slide deck, HyperFrames,
Live artifact, Image, Video, Audio); move the CLI/BYOK execution switcher
out of the composer footer up to the top bar beside Discord; restyle the
example preset cards into full-bleed thumbnail + name (dropping the prompt
blurb, trailing icon, and faux browser chrome); and stop showing the
"Example prompts: …" badge when a preset is applied.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* fix(web): derive Home sub-category pills from the displayed preset list

Addresses review: a selected child pill could render every preset card
instead of its slice. activeSubChips was built from the full install set
(pluginOptions) while filtering ran against the curated/sorted/sliced
activeExamplePlugins, so a facet category whose plugin wasn't in the Home
preset slice produced a pill that, when clicked, hit the all-cards
fallback and looked unfiltered.

Derive the pills from activeExamplePlugins (the same list that feeds the
cards) and drop the fallback, so every pill maps to a non-empty slice.
Update the rail test (preset card is now thumbnail + name, no prompt
blurb on the face) and add a fixture for a sub-category absent from the
displayed list.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
2026-06-04 16:06:22 +00:00
lefarcenandGitHub 4cfdc4d7db feat(desktop): play white brand splash video on startup (#3674)
* feat(desktop): play white brand splash video on startup

Replace the dark "Waiting for the web runtime URL…" placeholder with the
brand intro animation on a white background. The clip plays once and then
freezes on the settled "Open design" logo. The window only swaps to the web
runtime after the clip has had time to play through (MIN_SPLASH_MS), so it
never flips mid-animation, and it holds the final frame when the runtime
takes longer to boot.

The clip is embedded as a base64 data URL so it renders identically in dev
and in packaged builds without any electron-builder asset wiring — the
packaged app exposes no reliable on-disk asset path from the desktop main
bundle (the previous icon lookup already silently fell back to null there).

* feat(desktop): show splash in parallel with boot, reveal on web mount

Make the white brand splash a dedicated window so it can do real work:

- Packaged cold start now creates the splash BEFORE awaiting the daemon/web
  sidecars, so the animation is on screen during the whole boot instead of the
  user staring at no window at all for a few seconds. The 2s minimum splash now
  overlaps startup rather than being added on top of it.
- The main window starts hidden and loads the web app behind the splash. We
  reveal it only once the web bundle reports it has actually mounted
  (data-od-app-mounted="1") and the minimum splash time has elapsed, then close
  the splash. This removes the dark "Loading Open Design…" web shell that used
  to flash between the splash and the app. A hard timeout guarantees the user is
  never stranded on the splash if the mount signal never arrives.

tools-dev/dev keeps working unchanged: when no splash window is handed in, the
runtime creates its own.

* fix(desktop): measure splash hold from window creation, not adoption

PerishCode review: in packaged startup the splash window is created before
`await startPackagedSidecars(...)`, but the minimum-hold clock was stamped later
inside `createDesktopRuntime` (after the sidecars had finished booting). The
reveal then waited `MIN_SPLASH_MS` from that post-boot instant, so a fast web
mount still added almost another 2s on top of boot — re-introducing the very
delay the parallel splash was meant to remove.

`createSplashWindow()` now returns a `{ window, startedAt }` handle stamped at
creation; the packaged entry forwards `startedAt` via `splashStartedAt` and the
runtime measures the hold from there. Runtime-created dev splashes stamp their
own creation time, so the hold overlaps boot in every path.

* fix(desktop): gate DesktopRuntime.show() behind the splash reveal

PerishCode review: the main window now starts hidden and is revealed only after
the web app mounts, but `DesktopRuntime.show()` still called `window.show()`
unconditionally. The packaged entry routes a pending second-instance focus
through that method via `onDesktopReady`, so launching the app twice while the
sidecars/web are still booting would surface the half-loaded main window and
bypass the reveal gate — reintroducing the startup flash.

`show()` now focuses the splash while the runtime is pre-reveal and only shows
the main window once `revealWhenReady()` has fired.
2026-06-04 16:04:47 +00:00
475b9a480b fix(daemon): include user toolchain dirs in agent spawn PATH so shebang interpreters resolve (#3675)
What: `applyAgentLaunchEnv` now appends the user toolchain bin dirs
(Homebrew, ~/.local/bin, ~/.bun/bin, version-manager node dirs, npm
prefixes — the same set binary *resolution* searches) to the end of the
spawn PATH. Exposed via a new `userToolchainBinDirs()` from executables.ts;
injectable param defaults to the real dirs (tests pass []).

Why: agent detection resolved a binary's path using resolvePathDirs()
(PATH + wellKnownUserToolchainBins) but spawned it with a PATH containing
only nodeBinDir + the agent's own dir. An agent whose shebang interpreter
lives in a toolchain dir not on the daemon PATH then failed to execute:
Pi (`/usr/bin/env bun` via @oh-my-pi/pi-coding-agent) resolved fine but its
`--version` probe exited 127 ("env: bun: No such file or directory"), so
detection classified it not-invocable and marked Pi unavailable. claude/codex
only survived because their interpreter (node) happened to be on the spawn
PATH. Resolution and spawn PATH are now symmetric, fixing detection AND the
run/connection-test spawns that share this function.

How: append appendPathDirs into the PATH merge (lowest priority, deduped).
Verified: Pi flips unavailable -> available after the fix (path + version
now populated), alongside claude/codex.

Test: apps/daemon/tests/runtimes/launch.test.ts — "appends toolchain dirs
so shebang interpreters (e.g. bun) resolve at spawn time" (would go red
without the fix). Existing exact-PATH tests pass [] to stay deterministic.

Co-authored-by: ZC Deng <zhic.deng@gmail.com>
2026-06-04 15:51:49 +00:00
lefarcenandGitHub 9fd65efec0 fix(web): scope generation-preview override to the empty design-files tab (#3684)
Address review: dropping the DESIGN_FILES_TAB guard outright let the
generation card win for every in-flight run on the design-files tab,
hiding the file browser of a populated project until a previewable
artifact existed. Scope the override to the truly empty tab, mirroring
DesignFilesPanel's own empty-state gate (no files, no live artifacts, no
folders), so a populated project keeps browsing while generating.

Adds a regression test that a populated design-files tab still renders
file rows while a run is active.
2026-06-04 15:16:29 +00:00
lefarcenandGitHub 668d556097 ci(release): throttle the per-push nightly build instead of debouncing it (#3682)
`notify-release-feishu` builds a full nightly (via release-stable, channel=nightly)
and posts a Feishu card on every push to `release/**`. Its concurrency used
`cancel-in-progress: true`, so a newer push cancelled the in-flight build. When a
release branch is busy — pushes spaced closer than a full cross-platform build
takes — every build was cancelled by the next push and no nightly was ever cut.
(Observed on release/v0.10.0: a run of pushes all cancelled, the last successful
nightly was hours old.)

Switch to `cancel-in-progress: false` (throttle): the running build finishes, and
pushes that arrive during it coalesce — GitHub keeps the in-progress run plus only
the newest pending run, cancelling older pending ones. A busy release branch now
always cuts a nightly and converges on the latest commit, with at most one build
running plus one queued. Trade-off: a newer push no longer interrupts a running
build for an older commit; it waits one build cycle. For nightly that's the right
call.
2026-06-04 15:04:53 +00:00
1b47dcd22c feat(landing-page): add Solution → Roles pages (/for/<role>) for 5 audiences (#3658)
* feat(landing-page): add Solution → Roles pages (/for/<role>) for 5 audiences

Enable the Solution dropdown's Roles group (previously commented out) and ship
the five audience landing pages it points to: solo builders, designers,
engineers, product managers, and marketing teams.

Why: the Solution nav shipped with use-case pages but the Roles group was held
back with no destinations. Visitors arriving by job-to-be-done had no
"this is for someone like me" entry point, and the use-case pages alone don't
speak to a role's full workflow. Each role page is a conversion surface (role
search terms have ~zero volume, so these are not SEO-traffic pages) and an
internal-link anchor for the Solution cluster.

What users see: a Roles group at the bottom of the Solution dropdown with five
links. Each page has its own hero, a role-specific TL;DR, a three-step "how a
<role> uses Open Design" flow, a feature grid of real catalogue templates
curated per role (no overlap between roles — every role shows a distinct set of
interactive examples), a role-specific comparison table, and a role-specific
FAQ. All copy is translated across the 18 landing locales; the nav group label
is localized too. Pages reuse sub-page-layout and emit BreadcrumbList + WebPage
+ FAQPage JSON-LD with locale-aware canonical URLs.

Featured templates are picked by manifest id through pickSolutionPlugins, which
fails the build on any missing id, so a renamed or removed example breaks the
build loudly instead of shipping a short grid.

* fix(landing-page): align nav dropdown caret to the label's optical midline

The ▾ caret after each top-level nav label (Product, Solution, Agent, …)
rendered as a baseline-aligned inline glyph at 10px, so it dropped to the
lower-right of the label and read like a stray comma after each word —
especially visible with CJK labels.

Make the nav link an inline-flex row centered vertically, bump the caret to
11px with line-height 1, and nudge it up 1px to sit on the text's optical
midline. The hover/focus state keeps the same resting position (the color
shift to coral remains the open affordance) so the glyph no longer jumps.

* refactor(landing-page): split solution-pages-i18n into per-locale modules

The Roles pages added five role* keys with full prose across all 18 locales,
which grew solution-pages-i18n.ts to ~1.4 MB — over the repo's 1 MiB
MAX_CHANGED_FILE_BYTES blob guard (.github/workflows/ci.yml), making the
required "Guard changed file sizes" check (and the Validate workspace
aggregate that depends on it) red and the PR unmergeable.

Move each locale's copy block into solution-pages-i18n/<code>.ts and the
shared shapes into solution-pages-i18n/types.ts. solution-pages-i18n.ts is now
a thin index: it re-exports the public type API, imports the per-locale
modules, assembles BY_LOCALE, and resolves copy with the English fallback.

The public API (getSolutionPageCopy, SolutionPageKey, SolutionPageCopy, …) and
runtime behavior are unchanged. Largest split file is ~110 KB (ru), the index
is ~3 KB, so every tracked source file is now well under the 1 MiB limit.
Verified: typecheck clean, build green (8457 pages), and per-locale role-page
titles still render translated (zh/ja/de/ru/ar) rather than falling back to EN.

---------

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-06-04 14:49:58 +00:00
0767948f48 fix(web): tighten preview workspace panels (#3643)
* fix(web): tighten preview workspace panels

* test(web): align preview panel assertions

Generated-By: looper 0.9.2 (runner=fixer, agent=codex)

* fix(web): simplify handoff cli unavailable rows

* fix(web): clarify handoff framework selection

* fix(web): refine preview comment controls

* fix(web): align queued send strip with composer

* fix(web): balance mark toolbar wrapping

* fix(web): let mark note actions shrink

---------

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: Looper <looper@noreply.github.com>
2026-06-04 14:43:40 +00:00
8920577f43 feat(web): simplify Home composer (drop plugin-inputs form & example pill, move Design Agent next to Send) (#3645)
* feat(web): simplify Home composer (drop plugin-inputs form & example pill, move Design Agent next to Send)

- Remove the inline plugin-inputs form (ARTIFACT KIND / AUDIENCE / TEMPLATE) from the Home composer; required inputs no longer gate Send (defaults still flow to the backend).
- Remove the dismissible "selected example" pill above the editor; picking an example still seeds the prompt.
- Move the Design Agent toggle from the footer-left group to sit directly left of the Send button.

* fix(web): keep client-side required-input gate on Home composer

Dropping the inline plugin-inputs form left pluginInputsAreValid() always
returning true, so a required field with no default that the user blanks out in
an editable Home scenario kept Send enabled and only failed at click-time apply
with a generic "Failed to apply …" error (the daemon's validateInputs rejects
missing required inputs).

Restore a client-side gate that mirrors the daemon's required-input rule and
names the missing field in the submit error, so these Home flows fail fast.
Extracts the pure check into utils/pluginRequiredInputs with unit coverage.

* fix(web): drop example from active-row mount condition

With the example pill removed from the Home composer active row, keeping
selectedPromptExample in showActiveContextRow mounted an empty .home-hero__active
row when an example was picked with no other active context. Remove it from the
condition so the row only renders when it has visible content.

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
Co-authored-by: lefarcen <935902669@qq.com>
2026-06-04 14:08:40 +00:00
lefarcenandGitHub 9a18397090 fix(web): keep generation progress card on the design-files tab (#3671)
The right-side preview pane showed the empty "Creations will appear
here" file list while an agent was actively generating, instead of the
generation progress card with its understand/generate/prepare steps.

PR #3516 (Lexical composer rewrite) added an `activeTab !==
DESIGN_FILES_TAB` clause to `showGenerationPreview`. The design-files
tab is the default landing tab, so on a fresh project the in-flight
generation preview was suppressed and the idle empty state won the
surface. Before #3516 the preview branch rendered ahead of the
design-files branch with no tab guard.

Drop the clause so the progress card again takes priority over the empty
file list. `buildGenerationPreviewState` returns null once a previewable
artifact exists, so a finished artifact is never hidden behind the card.

Adds a red-spec FileWorkspace test that renders a generating run on the
design-files tab and asserts the generation-preview stage is shown.
2026-06-04 13:54:29 +00:00
lefarcenandGitHub e19c057d8d fix(web): flip deck preview to a queued send's marked slide (#3668)
* fix(web): flip deck preview to a queued send's marked slide

When several edits are queued against marked elements on different deck
slides, the preview stayed parked on slide 1 while the agent drained the
queue — the user couldn't see each edit land in context.

The mark's slide index was already captured into the comment snapshot and
target, but it was dropped when the snapshot became a ChatCommentAttachment,
so nothing downstream knew which slide a queued send touched. Carry
`slideIndex` through the contract and both attachment builders, then have
ProjectView arm a `slideNavRequest` (mirroring `shareRequest`) the moment a
queued send starts processing. FileWorkspace activates the deck tab if it is
open and FileViewer consumes the nonce once to flip the preview via the
existing `od:slide` go bridge, writing the cached slide state first so a
freshly-mounted iframe restores to the same slide on load.

No-op for plain prompts, free pins, or marks without a slide index.

* fix(web): keep deck slide-nav consume-once across viewer remounts

The consumed-nonce guard lived in a per-mount ref, so leaving the deck tab
and returning remounted HtmlViewer, reset the ref, and replayed the still-live
slideNavRequest — yanking the preview back to the queued slide and clobbering
manual navigation. Move the guard into shouldConsumeSlideNav, keyed by preview
state key outside the component, so "consume once" survives remounts. Add a
unit spec covering first-handling, fresh-nonce, and per-file isolation.

* fix(web): only flip a deck whose tab was open when the queued send started

slideNavRequest stayed live in parent state, so a request for a deck that was
closed at processing-start would resurface and jump the preview when the user
opened that file by hand much later — not the follow-along behavior the feature
promises. Decide deliverability once, at fire time: FileWorkspace marks the
nonce deliverable only when the target deck is already an open tab, and the
viewer is fed the request only for that deliverable nonce on the active file.
A closed deck never becomes deliverable, so opening it later cannot jump.

Pure helpers isSlideNavDeliverableNow / deliverableSlideNavForActiveFile carry
the decision, with specs for the closed-deck-opened-later case.
2026-06-04 13:48:03 +00:00
netteeandGitHub c6ee16be9d feat(daemon): pass open design trace env to AMR (#3667) 2026-06-04 13:37:17 +00:00
aa782365e0 feat(daemon): pass parentSession in pi RPC new_session for conversational continuity (#3434)
* feat(daemon): pass parentSession in pi RPC new_session for conversational continuity

Add parentSession option to attachPiRpcSession that sends
new_session { parentSession } before prompt on pi's stdin.
pi loads the prior .jsonl session file, forking conversation
history into the new session.

- resolveLastSessionPath(cwd) scans <cwd>/.pi/sessions/ for
  the most recently modified .jsonl
- piParentSessionPaths Map in server.ts persists the last
  session path per effectiveCwd
- Backward compatible — no new_session sent when no prior
  session exists
- 3 new tests covering stdin ordering, backward compat, and
  session path capture after agent_end

Closes #3414

* fix: replace str shorthand with string in parentSession test type annotations

* fix(daemon): scope pi session resume to conversations

* fix(daemon): handle pi parent session failures

* fix(daemon): clear stale pi session on capture miss

* fix(daemon): keep pi resume failure SSE code stable

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-06-04 13:20:50 +00:00
lefarcenandGitHub e260b56b30 fix(web): bottom-anchor the chat transcript so short and forked conversations don't strand at the top (#3669)
A short conversation — most visibly a fresh "Fork from here" branch — rendered its
messages stuck to the top of the chat log with a large empty area below, instead of
resting against the composer like a normal chat. Two causes:

1. `.chat-log` is a flex column with no vertical anchoring, so when the content is
   shorter than the viewport the messages sit at the top (~530px of dead space below).
2. The anchor-to-top reserve (`.chat-log-tail-spacer`, sized while a reply streams so
   the user's turn can reach the top) never collapsed once the turn finished. A short
   completed turn was left pinned at the top above ~540px of reserved blank.

Fix:
- Bottom-anchor the transcript with `margin-top: auto` on the first row. When content
  is shorter than the viewport the auto margin absorbs the slack and the messages rest
  against the composer; when it outgrows the viewport the margin resolves to 0 and the
  log scrolls normally with the top fully reachable. (Deliberately not
  `justify-content: flex-end`, which clips overflowing content above the scroll range.)
- Collapse the tail spacer when streaming ends, and reset it (plus the anchor flags) on
  conversation switch, so a finished or freshly opened short conversation settles cleanly
  instead of inheriting a stale reserve. Anchor-to-top during an active turn is unchanged.

Verified live (Playwright, local daemon+web): fresh short conversation last-message-to-
bottom gap 532px -> 20px; completed short turn tail spacer 549px -> 0; long conversation
still scrolls and reaches the top (scrollTop 0, first row at +18px).
2026-06-04 13:20:43 +00:00
lefarcenandGitHub b33ea26de3 fix(web): stop conversation list flashing "0 msg" while switching chats (#3663)
* fix(web): stop conversation list flashing "0 msg" while switching chats

The chat tab's conversation list derives the active conversation's message
count from the live `messages` array. Switching conversations resets that
array to [] while the new conversation's history is fetched, so the active
row rendered a phantom "0 msg" for the duration of the load (and stayed at 0
if the load failed) instead of its real count.

Trust `messages.length` for the active conversation only once the live array
actually reflects it (`messagesConversationId === activeConversationId`);
otherwise fall back to the persisted server `messageCount`. The live count is
still used during streaming so the active row stays fresh as turns arrive.

* fix(web): wire messagesConversationId for the other ChatPane mounts

SideChatTab and DesignSystemFlow also render the conversation list but did not
pass the new marker, so their active row fell back to the persisted count and
lost live-count freshness. Both own a single loaded conversation whose messages
always reflect the active id, so pass that id through to keep the active row's
live count authoritative (restores pre-change behavior). ProjectView still
passes its real messagesConversationId (null during the switch/load window).

* fix(web): keep secondary ChatPane mounts on the persisted message count

The SideChatTab and DesignSystemFlow mounts both have loader windows where the
live `messages` array does not reflect the active conversation —
useConversationChat resets to [] before a load, and DesignSystemFlow's loader
does not retag projectChatMessages on switch. Marking the live array as
authoritative there would reintroduce the same phantom/stale count this PR
removes. Omit messagesConversationId for those mounts so their list uses the
stable persisted conversation.messageCount; only ProjectView, which tracks the
marker precisely, opts into the live count.
2026-06-04 13:02:20 +00:00
7ab1e6eff2 feat(composer): reconcile context with prompt + per-connector brand color (#3625)
* feat(composer): reconcile context with prompt + per-connector brand color

Two increments on top of the Lexical composer (#3516):

- Filter contextPlugins/contextMcpServers/contextConnectors at submit time
  by whether the entity's @mention still survives in the prompt text. When a
  user deletes a mention pill, that plugin/MCP/connector is no longer sent to
  the agent. Adds mentionTokenPresent() to inlineMentions.

- Give connector mention pills a per-connector brand color (Notion black,
  Figma purple, etc.) by setting an inline --m-hue in MentionNode.createDOM,
  reusing the existing CSS hue machinery instead of the shared connector green.
  Adds a curated brand-color map with a hashed fallback.

* fix(composer): keep mention context when punctuation trails the pill

mentionTokenPresent() reused the parser's strict isMentionRightBoundary(),
so a still-visible atomic pill serialized as `@Slack,` was treated as
deleted and dropped from contextPlugins/contextMcpServers/contextConnectors
at submit time. Add a submit-time boundary that also accepts trailing
clause/sentence punctuation, leaving the parser invariant untouched, plus a
HomeView regression for the punctuation case.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* fix(composer): make connector pill brand hue theme-aware

Near-black curated brand colors (Notion, GitHub, Slack) rendered the
connector mention pill text as dark-on-dark in the app's dark theme,
since chat.css derives the text as color-mix(--m-hue 72%, --text) and
--text is light in dark mode. connectorBrandColor now takes the live
theme and lightens hues below a luminance floor so they stay legible,
while bright brands (Figma, Linear) pass through unchanged.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* fix(composer): make connector pill hue follow live theme switches

Recompute a mounted connector pill's brand --m-hue when <html data-theme>
or the OS prefers-color-scheme changes, not only on node create/update, so
a pill inserted in light mode no longer keeps its near-black hue (and
dark-on-dark text) after a live switch to dark mode.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* feat(composer): collapse footer tools into one "+" menu; drop duplicate context badges

- Replace the separate "@" context-trigger and paperclip attach buttons with a
  single "+" button. Its popup offers Attach files plus Connectors/Plugins/MCP
  right-side flyout submenus (Plugins/MCP searchable + scrollable). Picking an
  entry inserts the inline @mention pill via the existing pickX handlers; "Add …"
  rows route to the plugins / integrations views.
- Stop rendering the top plugin/connector/MCP context-badge row — those now live
  as inline mention pills in the composer, so the badges were duplicates. Staged
  file chips still render there. showActiveContextRow no longer keys off the
  selected*Contexts arrays.
- New i18n keys (addMenu/addPlugin/addConnectors/addMcp/noPlugins/noMcp/
  noConnectors) in types.ts + en + zh-CN; other locales inherit via ...en.

* fix(composer): repair collapsed-menu test selector and complete addMenu i18n

Update the HomeView context-picker test to open the plus menu before
asserting the attach affordance, since the collapsed footer now renders
home-hero-plus-attach only after home-hero-plus-trigger is clicked.

Add the seven home-hero add-menu keys (addMenu, addPlugin, addConnectors,
addMcp, noPlugins, noMcp, noConnectors) to every locale beyond en/zh-CN so
the i18n contract holds across all 18 locale files.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* test(web): drop assertions on removed context-badge row

The PR removed the duplicate plugin/connector/MCP context-badge row
(context now renders as inline @mention pills), but four HomeView tests
still asserted the dropped `home-hero-context-plugin-*` testids and went
red. Re-anchor them to behavior that survives: the submit payload's
`contextPlugins`/prompt-pill text for picker flows, and the active
context row's resolved-count label for empty-prompt "Use" handoffs.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* test(web): align entry-chrome e2e with collapsed footer menu

The footer-collapse change removed the always-visible home-hero-attach
control and the plugin/MCP/connector context-badge row, but the entry
chrome Playwright suite still asserted those removed surfaces, failing
Browser tests (and the Validate workspace aggregate).

- Assert home-hero-plus-trigger instead of the removed home-hero-attach.
- Drop the home-hero-context-plugin-* badge assertions; plugin context
  no longer renders a visible badge. Coverage is preserved by the
  existing prompt-text and submit-payload (pluginId) assertions.

* fix(composer): exclude inline-mentioned contexts from active row; reset plus-menu query per submenu

Gate the Home active context row on contexts that have no inline @mention
representation, so selecting only an inline-mentioned connector no longer mounts
an empty row above the editor. Reset the shared plus-menu search query when the
active submenu changes so a stale plugin search no longer filters the MCP list
into a false "No MCP servers" empty state.

Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code)

* fix(composer): keep context-only Use selections in submit payload

The submit-time reconciliation dropped every selected context that lacked an
inline @mention token, which silently discarded contexts staged through the
plain Use action (it never writes a token). The agent then received no
contextPlugins even though the user explicitly chose the plugin as context.

Tag each selection with inlineBacked: contexts inserted as @mention pills are
only sent while their token survives in the prompt; context-only Use selections
stay in the payload until explicitly cleared. The active context row likewise
counts only context-only selections, so an inline-mentioned-only pick no longer
mounts an empty row. Adds a regression covering the direct Use submit path.

* fix(composer): render removable chips for context-only Use selections

Context-only selections (staged via the plain Use action, no inline @mention
pill) are kept in the submit payload, but the home composer no longer rendered
any chip for them, so they were invisible and unremovable — a kept context
would silently drift into later freeform prompts.

Render a removable chip in the active context row for each context-only
plugin/MCP/connector selection, wired to the existing remove handlers. Adds a
regression that stages a plain Use context, clears it via the chip, and asserts
the submit payload no longer includes it.

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
Co-authored-by: lefarcen <935902669@qq.com>
2026-06-04 12:55:30 +00:00
lefarcenandGitHub e547ba28a4 fix(web): stop early analytics events shipping app_version=0.0.0 (#3609)
useAppVersion() reads the daemon-pinned version from /api/version at
runtime, but PostHog's app_version rode solely on the register()
super-property, which races that fetch. High-frequency early events
(page_view above all) captured before the version resolved shipped with
app_version='0.0.0' permanently — the dashboards showed 0.0.0 on every
build after the runtime-fetch approach landed.

resolveAppVersionForCapture() awaits a shared single-flight /api/version
fetch before every capture path (client init, super-property register,
track, consent re-init) and stamps app_version/ui_version onto each event
explicitly, so no event ships with the placeholder once the version is
knowable. The hook still returns the placeholder for first paint.

Red spec: tests/analytics-app-version.test.tsx gains
resolveAppVersionForCapture cases (resolves real version before capture;
no refetch when already real).
2026-06-04 11:58:12 +00:00
234a11aa05 feat(web): collapsible entry rail + quiet new-user templates + icon-only topbar chips (#3617)
* feat(web): collapsible entry rail + quiet new-user templates + icon-only topbar chips

Rework the entry view to keep new users focused on the hero composer:

- Nav rail collapses to a single topbar toggle by default and docks as an
  overlay on demand; a hover-reveal collapse button on the logo folds it back.
- For users with no projects, the Community templates gallery starts hidden
  behind a faint bottom hint; an upward scroll gesture or click smoothly
  reveals it, with a pinned collapse pill to fold it away again.
- Collapse the Discord / model-switcher / Use-everywhere topbar chips to
  icon-only with hover tooltips; the GitHub Star chip stays a full pill.

* test(e2e): expand collapsed nav rail before driving rail destinations

The entry nav rail now starts collapsed, so the UI smoke tests that click
`entry-nav-*` items (or assert the rail/logo is visible) timed out. Add an
idempotent `ensureRailOpen` helper and call it before rail nav interactions
across the e2e/ui suite; adjust the two rail-structure assertions to expand
first.

* test(e2e): reach home via Home nav and expand rail for help launcher

The logo now doubles as a hover-to-collapse control, so clicking it to return
home is intercepted by the collapse overlay — route via the explicit Home nav
item instead. Also expand the rail before opening the rail-footer help menu,
and guard the execution-pill nav loop with ensureRailOpen.

* fix(web): make collapsed rail inert and restore Discord chip title

Address review feedback:
- The collapsed nav rail kept its logo + nav buttons mounted and focusable, so
  a fresh Tab on the home view landed on invisible rail controls before the
  visible toggle/hero. Mark the rail `inert` while collapsed so its descendants
  leave the keyboard tab order and pointer flow; add a keyboard regression test.
- Restore the Discord chip's native `title` alongside `data-tooltip` so the
  existing topbar contract test (entry-topbar.test.ts) keeps passing.

* fix(web): make collapsed templates gallery inert and keep rail foldable on touch

Address second review pass:
- The collapsed new-user templates gallery was only aria-hidden + height-clipped,
  so its still-mounted buttons/tabs/search stayed in the keyboard tab order.
  Mark the gallery body `inert` until revealed (deps include `enabled` so it
  applies once the async-mounted body appears); add a keyboard regression test.
- On coarse-pointer / non-hover devices the hover-only collapse button plus the
  display:none topbar toggle could strand the rail open. Keep the collapse
  control visible + tappable under `@media (hover: none)`; add a CDP-emulated
  touch regression test.

* fix(web): keep inline switcher chip accessible name in icon-only topbar

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-06-04 11:54:39 +00:00
lefarcenandGitHub aba4b5bff2 fix(home,daemon): keep mode card on-screen and let plugin authoring pause on a question-form (#3640)
* fix(home,daemon): keep mode card on-screen and let plugin authoring pause on a question-form

Two release/v0.10.0 firefight fixes:

- Home Design Agent description card: the mode toggle's popover card is
  only shown on the home hero and anchors upward, so at its default 360px
  it crowds (and on smaller windows clips) the page header. Cap its height
  on the home hero so it always clears the top chrome and scrolls for the
  overflow.

- Plugin authoring 'ended before generating the required generated-plugin
  artifacts' error: the od-plugin-authoring turn-1 flow emits a
  <question-form> to collect the brief, then stops and waits for the user.
  That turn legitimately closes with code 0 and no artifacts yet, but the
  missing-artifacts guard treated it as a failure. Track whether the agent
  emitted a clarifying <question-form> in its streamed text and skip the
  guard in that case.

Red spec: chat-route.test.ts 'does not fail plugin authoring when the
turn-1 reply is a clarifying question-form awaiting the brief' (red on
main, green on branch).

* fix(daemon): recognize <ask-question> alias in plugin-authoring clarification guard

The web form parser (apps/web/src/artifacts/question-form.ts) accepts both
<question-form> and its <ask-question> alias as a valid brief form. The
plugin-authoring missing-artifacts guard only matched the canonical tag, so
a turn-1 reply using the alias still rendered a valid form in the UI but
fell through to the 'ended before generating…' failure. Mirror the parser's
accepted tag set in the daemon guard and add a regression test for the
alias path.

Addresses Looper review feedback on #3640.

* fix(daemon): gate plugin-authoring clarification carve-out on a renderable form body

The previous carve-out matched only the question-form/ask-question opening
tag, so a malformed (non-JSON) body — which the web parser keeps as raw
prose with no rendered form card — would still suppress the missing-artifacts
failure, turning a hard failure into a false success.

Mirror the parser's minimal renderable contract in a daemon-local helper
(emittedRenderableQuestionForm): a closed question-form/ask-question block
whose body parses as JSON to an object with a non-empty questions array.
Buffer the run's visible text and validate at close. Documented as a
deliberate mirror of apps/web/src/artifacts/question-form.ts since the app
boundary forbids importing it; promote to packages/contracts if they drift.

Tests: question-form and ask-question fixtures now use valid JSON bodies,
plus a new regression pinning that a non-renderable (non-JSON) form body
still fails. Addresses Looper review feedback on #3640.

* fix(daemon): keep question-form close-tag scan Unicode-safe in the daemon mirror

The mirrored renderable-form check lowercased the whole buffer before
indexing the close tag. Some code points expand under toLowerCase ('İ' ->
'i̇'), shifting every offset after them, so closeIdx desynced from openEnd
(original-string coordinate) and the JSON body slice was corrupted —
failing a valid form when a casing-expanding char preceded it.

Mirror the web parser's findCloseTag: scan char-by-char and lowercase each
fixed-length candidate slice, returning an index in the original-string
space. Added a daemon regression with an 'İ' preamble before a valid form.

Addresses Looper review feedback on #3640.
2026-06-04 11:43:06 +00:00
d6fe99bcdf Make existing project folders easier to open and iterate (#3529)
* Support opening and iterating existing project folders

* Fix existing folder flow i18n strings

* Stabilize AMR status sync

---------

Co-authored-by: Ben Stadler <ben@thebenstadler.com>
Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-06-04 11:35:49 +00:00
lefarcenandGitHub 5555b2eca9 fix(web): keep the streaming question-form id stable (#3582 follow-up) (#3644)
* fix(web): keep the streaming question-form id stable (no churn)

`parsePartialQuestionForm` derived the form `id` from the streaming body
`id` token, but that arrives char-by-char and `parsePartialJson` repairs the
open string, so `form.id` churned (`"d"` → `"di"` → …). `id` keys the live,
still-editable Questions panel (ProjectView's questionFormKey, QuestionForm's
data-form-id / input names), so each partial-id change remounted the panel and
discarded in-progress answers. Derive the preview id from the open-tag attr
(complete the instant the tag streams) or the stable default instead; the
final parse (tryParseForm, after the close tag) still adopts the real body id.
This also keeps the earlier nested-id-masquerade guarantee (no body scan).

Follow-up to #3582. Regression: the form id stays `discovery` while the body
id token streams in.

* fix(web): adopt the question-form body id once complete (no preview→final remount)

Building on the char-churn fix: when a form omits the open-tag id attribute
but carries a body id, the preview used 'discovery' and the final parse
adopted the body id — a single remount at the close tag that could drop
answers. Adopt the body id as soon as its string literal is terminated
(depth-1, nested ids ignored); that value equals what tryParseForm assigns,
so preview id == final id and the panel never remounts on the swap. The id
settles early (it's a leading field), before any question is answerable.

Regressions: a partial body id is not adopted (no churn); a complete body id
is adopted and matches splitOnQuestionForms' final id; nested question ids are
ignored.

* fix(web): decouple the question-form React key from the parsed form id

Root-cause fix for the identity churn three reviewers flagged. The Questions
panel was keyed on `${conversation}:${message}:${form.id}`, so any change to the
parsed id remounted it and dropped in-progress answers. The streaming preview
shows the `discovery` fallback id until the body id arrives, and a form that
emits answerable questions before its id settles flips identity while the user
is mid-answer — exactly the remount they described.

There is at most one (first) form per assistant message, so the id is redundant
for uniqueness: `${conversation}:${message}` already identifies the occurrence,
and a distinct later form lives in its own message (its own key, still replays
the reveal). Extract buildQuestionFormKey (id-free) and unit-test that the key
is stable across a streaming id flip, distinct across messages, and null until
conversation/message/form are all present.

completeTopLevelString stays — it settles the *displayed* id early — but the id
no longer drives identity, so even the questions-before-id ordering can't
remount the panel.
2026-06-04 11:22:36 +00:00
lefarcenandGitHub d0bca92646 fix(conversation-forking): fork from the in-memory snapshot so an unpersisted turn no longer silently fails (#3648)
"Fork from here" creates a new conversation by asking the daemon to copy the
source conversation's messages up to the fork point, looked up in SQLite by
`forkAfterMessageId`. When the fork point is an assistant turn whose run
errored or had its connection reset before the message was written to the
database (common with AMR/ACP "Connection reset by server"), that id is not in
the DB. The daemon returned 404 "fork message not found", `createConversation`
turned the non-OK response into `null`, and `handleForkFromMessage` threw into
a quiet error state — so no new conversation opened and the user never switched
into the fork. The page just flashed and stayed put.

Make fork copy what the user is actually looking at: the chat "Fork" action now
sends the in-memory messages slice (up to and including the fork point) as
`seedMessages`, and the daemon prefers that snapshot over reading the source
conversation from the DB by id. The Side Chat path (no `seedMessages`) is
unchanged and still seeds from `seedFromConversationId`.

Red spec: apps/daemon/tests/projects-routes.test.ts "forks from a
client-supplied snapshot when the fork point was never persisted" — 404 on
main, 200 on this branch.
2026-06-04 11:04:52 +00:00
AmyandGitHub 2afe7c7713 ci(e2e): tune main UI regression workflow alerts (#3601)
* ci(e2e): tune main UI regression workflow alerts

* ci(e2e): shard nightly UI regression runs

* ci(e2e): split P0 UI regression by domain

* ci(e2e): include entry configuration P0 shard

Generated-By: looper 0.9.2 (runner=fixer, agent=codex)
2026-06-04 11:00:22 +00:00
3a21392c46 Fix Electron composer lag and stale generation failures (#3593)
* Keep chat typing and web CSS resolution responsive

Cache the normalized mention index by entity array and scan only concrete @ positions so the composer does not rescan every file/plugin/skill token on each draft update. Link the shared components package at the root importer so Turbopack/PostCSS can resolve the exported component stylesheet from the web CSS entrypoint.

Constraint: Chat composer typing updates run on every keystroke in Electron, and tools-dev/Next evaluates global CSS from the workspace root.
Rejected: Debouncing textarea state updates | would make text entry feel stale and would not remove the expensive synchronous parser path.
Rejected: Relative-importing package source CSS from apps/web | would bypass the package export contract and hardcode package layout into the app stylesheet.
Confidence: high
Scope-risk: narrow
Directive: Keep inline mention parsing proportional to draft mention markers, and keep package CSS imports resolvable through workspace package links.
Tested: pnpm install
Tested: pnpm --filter @open-design/web test -- tests/utils/inlineMentions.test.ts
Tested: pnpm --filter @open-design/web typecheck
Tested: pnpm --filter @open-design/web build
Tested: pnpm guard
Not-tested: Full web Vitest remains blocked by unrelated CSS selector tests observed before this commit.

* Keep composer typing off the render path

Move keystroke-time draft updates into the textarea/ref path while deferring React draft rendering, mention overlay work, picker filtering, and localStorage persistence so Electron input echo stays within frame budget.

Constraint: Electron chat input was visibly lagging for plain text and mention text while the right iframe stayed responsive.

Rejected: Keep the textarea fully controlled | every keystroke still synchronizes the largest ChatComposer render path.

Confidence: high

Scope-risk: moderate

Directive: Keep prompt submission, slash commands, and mention insertion reading draftRef when draft rendering is deferred.

Tested: pnpm guard; pnpm --filter @open-design/web typecheck; pnpm --filter @open-design/web build; pnpm exec vitest run -c vitest.config.ts tests/components/ChatComposer.infinite-render.test.tsx; pnpm exec vitest run -c vitest.config.ts tests/components/ChatComposer.context-pickers.test.tsx; pnpm exec vitest run -c vitest.config.ts tests/utils/inlineMentions.test.ts; Electron inspect eval plain text avg 0.03ms max 0.5ms; Electron inspect eval @ path avg 0.03ms max 0.1ms; HTTP smoke status 200 with no components CSS resolve error.

Not-tested: Full web Vitest suite and Playwright UI suite.

* Keep Electron chat input responsive under heavy projects

Constraint: Electron project views can carry large chat/file DOM and Chromium can throttle or defer visible renderer work under tab/window visibility edge cases.
Rejected: Re-rendering the composer draft on every plain keystroke | it couples textarea input to mention overlay, resize, storage, and project layout work.
Confidence: medium
Scope-risk: moderate
Directive: Keep plain text input on the textarea/ref path; only commit React draft state immediately for mention, slash, chip, or imperative draft mutations.
Tested: pnpm guard; pnpm --filter @open-design/web typecheck; pnpm exec vitest run -c vitest.config.ts tests/components/ChatComposer.infinite-render.test.tsx; pnpm --filter @open-design/desktop test; pnpm --filter @open-design/desktop typecheck; desktop inspect actual project composer with OS SendKeys.
Not-tested: Push/PR remains blocked by GitHub 403 for mimicryluden on nexu-io/open-design.

* Keep heavy project typing isolated from preview layout

Constraint: Electron project panes can carry large DOM histories and preview frames that make textarea paint sensitive to parent layout.

Rejected: Further debouncing ChatComposer input state | app-side handler costs were already low while native textarea updates still lagged.

Confidence: high

Scope-risk: moderate

Directive: Keep the composer mounted through the fixed layer/slot pair so future layout changes do not pull typing back into the split-pane flex reflow path.

Tested: pnpm --filter @open-design/web typecheck; pnpm exec vitest run -c vitest.config.ts tests/components/ChatComposer.infinite-render.test.tsx tests/components/ChatComposer.context-pickers.test.tsx; pnpm guard; pnpm --filter @open-design/web build; desktop inspect fixed-layer DOM and SendKeys probe avg beforeinput->input 1.4ms max 2.5ms

Not-tested: PR publish blocked by GitHub repository permissions if remote still rejects mimicryluden

* Keep stale run recovery from hiding generated previews

Constraint: Completed daemon runs can outlive the in-memory run registry after a restart.

Rejected: Replaying succeeded rows with empty producedFiles | It turns completed messages into failed messages when the registry is gone.

Confidence: high

Scope-risk: narrow

Directive: Only active run statuses should enter daemon reattach recovery.

Tested: vitest generation-preview; vitest ProjectView reattach restore; web typecheck; pnpm guard; web build; desktop DOM eval failureText=false iframeCount=2

Not-tested: Fresh provider generation against an external model

* Keep composer contracts aligned with latest main

Rebase conflict resolution restored the current composer contract while leaving the lower-risk latency fixes in separate layers.

Constraint: The branch was rebased over newer workspace/session context composer contracts.

Rejected: Keeping the older composer conflict side because it removed current ChatComposer props and broke web typecheck.

Confidence: high

Scope-risk: narrow

Directive: Preserve upstream ChatComposer contracts when rebasing input-latency work.

Tested: web typecheck; targeted ChatComposer, inlineMentions, ProjectView generation-preview tests; desktop window-chrome test

Not-tested: Full browser interaction after fork push

* Fix daemon Nix workspace symlink pruning

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-06-04 10:47:44 +00:00
lefarcenandGitHub 4e17440f81 fix(web): keep empty persisted folders visible in Design Files (#3358 regression) (#3631)
* fix(web): keep empty persisted folders visible in Design Files

`folders` prop, so an empty persisted folder (user-created, or an empty dir in
an imported working directory) vanished — it couldn't be opened or deleted
until a file lived inside it. Thread the fetched `folders` back into
DesignFilesPanel and merge them into `dirsAtCurrentDir`; don't show the
project-empty state when folders exist; and treat a persisted folder as a
valid currentDir so navigating into an empty one doesn't bounce to the root.
FileWorkspace passes `folders={projectFolders}` (already fetched).

Regressions: an empty persisted folder shows at the root, and a nested empty
folder is navigable.

(cherry picked from commit 8f45bb14ccd14b04fb281c3b0eea06a0f5021446)

* fix(web): reset project folders when switching projects

With persisted folders now rendered, FileWorkspace kept the previous project's
folder list until the async fetchProjectFolders resolved, so switching to a
different (empty) project briefly showed the old folders and suppressed the
new project's empty state. Clear projectFolders synchronously on projectId
change, before the fetch.

Regression in FileWorkspace.test.tsx: rerendering with a new projectId while
the new folder fetch is pending drops the previous project's folders.

* fix(web): reset project folders during render, not in an effect

mrcfps flagged that the effect-based folder reset still let the new
DesignFilesPanel (keyed by projectId) mount once with the previous project's
folders before the effect cleared them — briefly suppressing the new project's
empty state, the exact regression this follow-up removes. Move the reset into
the render phase (adjust state during render via a projectId ref, mirroring the
designFilesNav reset), so the new panel never observes stale folders. A stable
EMPTY_PROJECT_FOLDERS constant keeps the render-phase setState idempotent.

Tightens the regression: a wrapper records the folders prop on every
DesignFilesPanel render, and the test asserts no render for the new project
ever sees a non-empty folder list — the pre-paint frame RTL's post-rerender DOM
check can't observe. Red on the effect-based reset, green here.
2026-06-04 10:41:29 +00:00
Tom HuangandGitHub fd1c0ecb93 Feat/optimize style (#3624)
* Refactor ChatComposer and DesignBrowserPanel components for improved functionality and localization

- Introduced new functions in ChatComposer for managing attachment orders and appending staged attachments, enhancing user experience during file uploads.
- Updated ChatPane to clarify the search button's aria-label for better accessibility.
- Enhanced DesignBrowserPanel with new localization keys for browser use inputs and actions, improving clarity across multiple languages.
- Adjusted styles in various components for better layout and user interaction, including updates to the chat input and staged context elements.

* Update AvatarMenu and Chat components for improved functionality and localization

- Modified AvatarMenu to include an optional section parameter for the onOpenSettings callback, enhancing flexibility in opening settings.
- Updated tests for AvatarMenu to verify the correct section is passed when opening settings.
- Adjusted styles in various components, including the ChatComposer and FileViewer, to improve layout and user interaction.
- Enhanced localization strings across multiple languages to reflect changes in chat mode terminology from 'Chat' to 'Ask', improving clarity for users.
2026-06-04 10:22:39 +00:00
goododeo-specandGitHub 18169ee52a Improve export fidelity contract for editable PPTX and PDF readiness (#3628)
* Improve export fidelity contract

* Improve export fidelity contract

* Improve export fidelity contract

* Improve export fidelity contract

* Wait for CSS images in desktop print handshake

* Wait for CSS images in desktop print handshake
2026-06-04 09:13:50 +00:00
lefarcenandGitHub 6e10fa4852 feat: stream agent questions token-by-token (#3582)
* feat(web): stream AskUserQuestion and discovery questions token-by-token

Render both agent question surfaces as they generate instead of popping in
only once the whole payload finishes.

- AskUserQuestion (Claude tool): builds on the live tool-input streaming
  landed in #3382. When a still-streaming tool's name is AskUserQuestion,
  render a read-only StreamingAskUserQuestionCard that grows its questions
  and options from the repaired JSON prefix; the interactive (persisted)
  card takes over once the full tool_use lands and the id leaves
  liveToolInput.
- <question-form> discovery (any agent, incl. Codex): the streaming parser
  now repairs the truncated prefix so a question shows the moment its label
  exists and its options grow in one at a time, instead of waiting for each
  question object to fully close. The Questions panel UX (skip countdown,
  auto-continue, reveal) is unchanged.

Shared repairJsonPrefix/parsePartialJson live in runtime/partial-json.ts.
No daemon, contract, or provider changes — this reuses #3382's
tool_input_delta / liveToolInput plumbing.

* fix(web): harden streaming <question-form> parser (review follow-ups)

Two merge-safe follow-ups from review on parsePartialQuestionForm:

- Derive form-level id/title/description from the parsed top-level object
  instead of regex-scanning the whole partial body, so a nested question or
  option id/description can no longer masquerade as the form's own. form.id
  keys the live Questions panel (ProjectView), so a mid-stream identity
  change would remount it. Removes extractJsonStringField.
- Strip a (possibly partial) trailing ``` fence so the live preview keeps
  streaming in the gap between the fenced JSON body closing and the
  </question-form> close tag arriving, instead of dropping back to empty.

Both covered by regression tests in question-form.test.ts.

* fix(web): preserve submitLabel + string-safe fence trim in streaming form parser

Second round of review follow-ups on parsePartialQuestionForm:

- Carry top-level submitLabel through the streaming preview, mirroring
  tryParseForm, so a custom CTA label doesn't flicker in only once the
  close tag arrives.
- Only strip the trailing ``` fence when those backticks are the closing
  wrapper, not content of a JSON string value still being typed — a label
  like "Use ```" mid-stream kept losing its backticks. The trim now checks
  it isn't inside an open JSON string first.

Both covered by regression tests in question-form.test.ts.

* fix(web): honest status badge on WebFetch/WebSearch cards

WebFetchCard / WebSearchCard hard-coded a green check badge regardless of
the real tool state, so a slow or failed fetch/search rendered as
false-green. Thread result / runStreaming / runSucceeded through both and
reuse ResultBadge (as the other tool cards do) so the badge reflects the
actual outcome.

Pre-existing on main (only surfaced in this PR's changed range because the
new streaming card additions shifted these lines down); folded in here
since it's a one-line-per-card fix in a file this PR already touches.

* fix(web): keep partial-JSON repair stable across a dangling string escape

repairJsonPrefix() appended a closing quote even when the stream stopped on
an incomplete escape inside an open string, leaving invalid JSON: `{"a":"x\`
became `{"a":"x\"` (the quote got escaped) and `{"a":"x\u` became an invalid
`\uXXXX`. parsePartialJson() then returned null, collapsing the live
AskUserQuestion / question-form preview to its empty frame mid-stream
whenever a prompt mentioned a Windows path, regex, or escaped quote.

Now neutralize a trailing incomplete escape before closing the string: drop a
partial `\uXXXX` (0-3 hex) and a lone (odd-run) trailing backslash. Completed
escapes and even backslash pairs (e.g. `C:\\Users\\`) are preserved.

Regression tests in ask-user-question.test.ts cover the repair cases and a
path-like prompt that keeps its last visible content.

* fix(web): trim unfinished scalar values in partial-JSON repair

repairJsonPrefix() only trimmed dangling commas / empty `"key":`, so a delta
that stopped mid-scalar left invalid JSON: `{"…","multiSelect":f` closed to
`{…:f}` and failed to parse, collapsing the live preview whenever a boolean,
null, or number token split across deltas. The cleanup loop now also trims a
partial `true`/`false`/`null` and a number cut on `.`/`e`/sign, leaving the
`"key":` / `,` / `[` boundary for the next iteration. Complete literals
(`true`, `12`, `1.5`, `1e3`) and string values that merely start like a
literal are not prefixes of these patterns, so they survive.

Regressions added in both the AUQ and question-form suites (a boolean split
across deltas keeps already-visible questions) plus repair-level unit tests.

* fix(web): stable preview question ids while a form streams

shapeStreamingQuestions surfaced a question as soon as its label existed, but
mapRawQuestion falls back to q${index+1} until the real id streams — so a
question that arrives label-first and gains its canonical id later would
change id mid-stream. The Questions panel stays editable while building and
keys fields / answers off q.id, so that id change remounts the field and
orphans whatever the user already picked. Pin the preview id to the array
index for the whole streaming phase (questions only append once their label
exists, so the index is stable); the final non-preview parse still uses the
real id.

Regression in question-form.test.ts: label-first then id-later keeps id q1.

* fix(web): preview question ids match the final parse (no orphaned answers)

The previous index-pin kept ids stable *within* the preview but still
differed from the final canonical id, so an answer entered while the panel
was building got stranded at the preview→final swap (field re-keys from `q1`
to `platform`, reads nothing). Surface a streaming question only once its
canonical id is determinable instead: use the streamed `id`; or, for an
already-closed (not in-flight) object with no id, the `q${index+1}` fallback
that the final parse will also assign; hold the in-flight object until it
gains an id. Preview id now always equals the final id.

Updated/added regressions in question-form.test.ts: a label-first object is
held until its id is determinable, and preview ids match splitOnQuestionForms.

* fix(web): stable positional keys in the streaming AskUserQuestion card

The streaming card keyed fields on q.question and options on opt.label, but
those strings grow token-by-token, so every delta changed the key and
remounted the subtree — replaying the reveal animation on each token instead
of mounting once. Switch to positional keys (questionIndex /
questionIndex:optionIndex); questions and options only append during
streaming, so the index is stable and each node updates its text in place.

Regression: StreamingAskUserQuestionCard.test.tsx asserts the field/option
DOM nodes are reused (===) across successive grown prefixes.

* fix(web): stream a closed no-id question before the close tag

The previous gate held *every* last no-id question as if in-flight, but a
last object whose braces have already streamed has a final fallback id
(q${index+1}) and should appear immediately — holding it broke the
"question shows the moment its label exists" behavior for forms that omit
explicit ids. Distinguish closed vs still-open via a string-aware
balanced-object count: surface a question once its object has closed (id
final) or it already carries an `id` (stable); keep holding only an
in-flight object with no id yet.

Regressions: a single closed no-id question streams pre-close-tag (id q1),
while the same content with an unclosed object stays held.

* fix(web): suppress hedging fallback text while a live AskUserQuestion streams

suppressAskUserQuestionFallbackText ran on the persisted blocks before the
liveBlocks pass, so during streaming (no persisted tool_use yet) seenAskUser
Question never flipped and any duplicate hedging markdown survived into
persistedBlocks while the live card was appended after — the exact
duplicate-question state the card removes, just earlier in the turn. Seed the
suppressor with whether a live AskUserQuestion is present in liveToolInput so
it fires during the streaming phase too.

Regression in AssistantMessage.test.tsx: fallback prose coexisting with a live
AskUserQuestion in liveToolInput is suppressed, while normal prose without a
live AUQ is kept.

* fix(web): order-aware fallback suppression for live AskUserQuestion

The previous fix seeded the suppressor with "a live AskUserQuestion exists",
which dropped ALL text blocks in the turn — including legitimate preamble
("I need one clarification:") that precedes the tool call, not just duplicate
hedging. Make it order-aware instead: suppress text only after an
AskUserQuestion in block order, and have the suppressor recognize a live
(streaming) AskUserQuestion live-tool block the same way it recognizes a
settled tool-group. Preamble before the card is kept; hedging after it is
dropped — matching the settled-case semantics.

Regression updated in AssistantMessage.test.tsx: intro preamble coexisting
with a live AskUserQuestion in liveToolInput is preserved.

* fix(web): place the live AskUserQuestion at its real stream position

The order-aware pass still appended the live AUQ block after all persisted
blocks, so hedging text the model emits *after* starting the tool call (which
is already in events, hence before the appended block) was kept. Carry the
event index the tool started at (`seq`, recorded in liveToolInput when the
first input_json_delta arrives) and split the events around it: blocks before
`seq` are preamble, the live AUQ block goes next, blocks after `seq` are
hedging. Suppression then keeps the preamble and drops the trailing hedging,
exactly matching the settled tool_use case.

Regression in AssistantMessage.test.tsx: with the tool starting after the
preamble (seq=1), preamble before the card is preserved and hedging after it
is dropped.

* fix(web): account for buffered preamble when pinning live-tool seq

`seq` was read from `latestAssistantMsg.events.length`, but preamble text goes
through `appendTextEvent` and isn't committed to `events` until the next
rAF/250ms flush. When intro prose and the first `tool_input_delta` arrive in
the same burst, that read undercounts by one, so the live AskUserQuestion was
positioned before the preamble and suppression dropped it as hedging. The
buffer now exposes `hasPendingText()`, and seq adds 1 for a still-pending
preamble chunk (it commits as one text event just before the tool position).

Regression in buffered-text-pending.test.tsx: appended text is reported
pending and absent from events until flush, then commits as one event.

* fix(web): re-render the streaming row on live tool-input deltas

AssistantMessage is memoized with a prop allowlist that omitted liveToolInput,
so a burst of pure tool_input_delta traffic (which only changes liveToolInput,
not message) was swallowed by the comparator — the live AskUserQuestion card /
code box wouldn't grow until some unrelated change, usually the final
tool_use. Add liveToolInput to ASSISTANT_MESSAGE_COMPARED_PROPS, and have
ChatPane pass it only to the streaming row (undefined elsewhere) so just that
row re-renders per delta, not all N messages.

Also widens ChatPane's liveToolInput type with the optional seq field.

Regression in AssistantMessage.test.tsx: with the same message object,
changing only liveToolInput re-renders and grows the card prompt.
2026-06-04 09:11:28 +00:00
chenxueandGitHub b1fd529eea Feat/aihubmix byok provider (#3583)
* feat(media): integrate AIHubMix as full-stack BYOK provider

Add AIHubMix (https://aihubmix.com) as a BYOK provider at the same layer as
SenseAudio — both a chat protocol and a media-generation provider — so users
can paste one key and get chat + image + speech, with the same key auto-mirrored
into media-config for the `od media` CLI path.

AIHubMix is an OpenAI-wire-compatible aggregator. Its distinguishing trait is the
fixed APP-Code attribution header (discount), funnelled through a single
aihubmixHeaders() helper so every outbound call point (chat proxy, connection
test, model discovery, media renderers, memory extractor) injects it in one place.

Chat protocol layer:
- Extend ApiProtocol / ConnectionTestProtocol / MemoryExtractionProvider unions.
- New web provider client + dispatch; mirror across apiProtocols, config,
  utils, Settings/ChatComposer/HomeHero UI surfaces.
- Refactor the SenseAudio tool-loop chat proxy into a shared
  registerByokToolChatProxy() factory; register senseaudio + aihubmix through it
  (senseaudio behaviour byte-preserved). connectionTest + providerModels treat
  aihubmix as OpenAI-shaped with APP-Code headers.

Media layer:
- Register provider + image (aihubmix-gpt-image-1 / -dall-e-3) and speech
  (aihubmix-tts-1) models; prefixed ids stay unique and map to wire names.
- OpenAI-compatible renderers (/v1/images/generations, /v1/audio/speech) and
  chat tool executors; ENV_KEYS + media-picker allowlists updated.

Note: AIHUBMIX_APP_CODE in apps/daemon/src/aihubmix.ts is an empty placeholder —
fill it with the code from https://aihubmix.com/appstore to enable the discount
header (the integration works without it).

Tests: media-aihubmix.test.ts (wire-name mapping, APP-Code invariant, image +
speech renderers) and an AIHubMix proxy-route case; verify:media-models, guard,
and typecheck all green.

* feat(media): source AIHubMix model lists from live catalog endpoints

Chat LLM models and media image models now come from AIHubMix's live catalogue
(GET /api/v1/models?type=llm | image_generation, public, returns model_id /
model_name) instead of only the hand-curated static lists.

Chat: the existing /api/provider/models discovery, when protocol=aihubmix, now
hits the type=llm catalogue and parses the AIHubMix envelope (342 models live).
The static SUGGESTED_MODELS remain as the pre-fetch default.

Media image: new GET /api/media/providers/aihubmix/models endpoint proxies the
type=image_generation catalogue (53 models), prefixes ids `aihubmix-` for
uniqueness, and caches for 5 min with stale-on-error fallback. generateMedia
synthesizes a def for any unregistered `aihubmix-*` id (like the fal-ai/* path)
so every catalogue model renders through the OpenAI-compatible image endpoint.

Helpers (aihubmixCatalogUrl / parseAIHubMixCatalog) + tests for url building,
envelope parsing, and rendering a dynamic catalogue id. The web media pickers
still read the static seed list; wiring them to the live endpoint is a
follow-up UI change.

* feat(web): show live AIHubMix image catalog in all media pickers

Wire the AIHubMix image-generation catalogue (53 models, fetched from the new
daemon endpoint) into every image-model picker. A shared hook
(useAIHubMixImageModels) fetches once per session and caches; mergeAihubmixImageModels
replaces the two static aihubmix seeds with the live list (and falls back to the
seeds when the fetch hasn't resolved or fails, so the picker is never empty).

Surfaces:
- NewProjectPanel image picker — merged list passed to supportedModels('image').
- Home media composer — buildHomeMediaComposer / fieldsForSurface /
  normalizeHomeMediaInputs now accept an injected imageModels list; HomeView
  fetches and threads it through all three composer call sites.

Saved/template image-model ids starting with `aihubmix-` are accepted even
though they're absent from the static IMAGE_MODELS, since they render
dynamically daemon-side.

* feat(byok): load AIHubMix chat model list without an API key

The BYOK chat model dropdown already merges live-fetched models with the static
suggestions, but the fetch was gated on a non-empty API key (every provider hits
/v1/models behind its key). AIHubMix's catalogue is public, so its 342-model
list now loads as soon as the AIHubMix tab is selected — no key required.

- Daemon /api/provider/models accepts an empty apiKey when protocol=aihubmix
  (baseUrl still required); providerModelsHeaders sends no Bearer when the key
  is blank.
- Web: missingByokModelFetchFields treats apiKey as optional for aihubmix, and
  the auto-fetch effect skips the commit-on-blur gate for aihubmix (base URL is
  prefilled and there's no key to debounce), so the list populates on tab select.

* feat(byok): default AIHubMix chat model to claude-opus-4-8

* feat(byok): memory model picker uses live searchable model list

The Memory model picker was a plain <select> hard-wired to the 8 static
SUGGESTED_MODELS. Swap it to the same SearchableModelSelect the chat picker
above uses, fed by the parent's merged fetched+suggested model options
(apiModelOptions). For AIHubMix that means the full live 342-model catalogue
shows as a searchable dropdown (keyless), matching the chat picker; other
protocols get their live /v1/models list once the chat key is set. 'Same as
chat' leads the list and 'Custom…' is pinned last.

* feat(byok): BYOK chat image-model picker lists live AIHubMix catalog

The Settings BYOK 'image generation model' dropdown (byokImageModel, used by
the chat-injected generate_image tool) listed only the 2 static aihubmix seeds.
Feed it the live image catalogue (useAIHubMixImageModels) for aihubmix so it
shows all ~53 models; SenseAudio keeps its static registry list. Relax the
daemon's isAIHubMixImageModel to accept any aihubmix-* id (they all render via
the OpenAI-compatible endpoint with prefix-strip), so a dynamically-chosen
default passes validation instead of being dropped back to the seed default.

* refactor(byok): share one hook for BYOK image-model pickers; wire composer

The chat composer's inline 'image generation model' picker still listed only
the static aihubmix seeds. Rather than re-derive the merge+filter a third time,
extract useByokImageModelOptions(provider) into the shared aihubmix-image-models
module (live AIHubMix catalogue for aihubmix, static registry otherwise) and use
it from both the Settings image picker and the composer picker. One cached fetch
feeds all sites.

* feat(byok): route AIHubMix chat by model name to native protocol wire

Per the AIHubMix integration guide §4.3, route the BYOK chat by model name to
each family's native endpoint on the AIHubMix origin instead of forcing every
model through /v1/chat/completions:
  claude*           → Anthropic /v1/messages (x-api-key + anthropic-version)
  gemini*/imagen*   → Gemini :streamGenerateContent (x-goog-api-key)
  everything else   → OpenAI /v1/chat/completions (keeps the tool loop)
All three carry the APP-Code attribution header.

The claude/gemini routes run base chat only (no in-chat generate_image yet) —
each protocol has a pluggable tool slot for a later Anthropic tool_use / Gemini
functionCall adapter. The OpenAI family keeps in-chat image/speech generation.

Reuse: extracted runAnthropicChatStream / runGeminiChatStream (+ payload
builders) from the existing /api/proxy/{anthropic,google}/stream handlers; those
routes now delegate to the same functions (behaviour preserved, 79 proxy tests
green). New helpers classifyAIHubMixModel / aihubmixOriginFromBase /
aihubmixAppCodeHeader in aihubmix.ts. No SDK added.

Default AIHubMix chat model is now gpt-5.5 (an OpenAI-family model) so the
default keeps in-chat generate_image working after routing.

Tests: claude→/v1/messages and gemini→/gemini/.../streamGenerateContent routing
assertions in proxy-routes.test.ts.

* feat(byok): AIHubMix in-chat image/video/audio model selection + media-adapters

Bring the BYOK chat media surface to parity across image, video and audio:

- Composer + Settings dropdowns for image/video/speech models and TTS voice,
  threaded as byokVideoModel / byokSpeechModel / byokSpeechVoice. The user's
  explicit pick is authoritative over the LLM's tool `model` arg (regression
  the LLM kept overriding the composer choice). "Default" resolves to the first
  live-catalogue model (honest label) with the daemon const as final fallback.
- New apps/daemon/src/media-adapters: injection-style capability registry +
  family-aware video request builder (seedance content[] array vs generic
  input_reference), capability shape aligned with OpenRouter /videos/models.
  The chat video executor now builds requests through it.
- Per-family request shaping:
  * video: duration snapped to each family's allowed set (veo 4/6/8, sora
    4/8/12, wan 5/10); i2v reference resolved to a data URL.
  * image: gemini/nano-banana models routed to gemini-native generateContent
    (the /v1/images/generations shape 400s for them); others stay OpenAI-shaped.
  * speech: gemini TTS via generateContent with PCM->WAV wrapping and Gemini
    voice fallback; others use /v1/audio/speech (mp3).
  * download: same-origin AIHubMix asset URLs re-send Bearer + APP-Code.
- Clear, actionable errors for unsupported i2v / "params dropped" models, plus
  per-call submit logging (model + endpoint) for multi-model verification.
- i18n keys across 19 locales for the new pickers and the neutral default label.

Tests: byok-tools + media-adapters specs (85 passing); guard + daemon/web
typecheck green.

* feat(byok): media tool loops on claude/gemini chat + wan i2v wire + seedance resolution fix

Make AIHubMix in-chat media generation work regardless of which chat model is
selected, and fix two image-to-video wire-shape bugs surfaced by live testing.

chat-routes: the aihubmix routeByModel divert sent claude -> Anthropic
/v1/messages and gemini -> Gemini generateContent BEFORE the OpenAI tool loop,
so those chat models had no generate_image/video/speech tools ("没有音频工具").
Add per-protocol tool loops inside registerByokToolChatProxy (reusing the shared
executeOneTool): runAnthropicToolTurn/Chat parse tool_use content blocks +
input_json_delta; runGeminiToolTurn/Chat parse functionCall parts. The shared
single-pass runAnthropicChatStream/runGeminiChatStream and the direct
/api/proxy/{anthropic,google}/stream routes are untouched. Factor the per-kind
tool-result hint into buildToolResultContent (shared by all three protocols) and
add openaiToolsToAnthropic / openaiToolsToGemini schema converters (Gemini schema
sanitized to the OpenAPI subset). Gemini 3.x attaches a thoughtSignature to each
functionCall part that must be echoed back verbatim or the follow-up 400s, so
replay the raw functionCall part instead of reconstructing {name,args}.

media-adapters: seedance accepts resolution only as a token (480p/720p/1080p),
never a WxH pixel string — passing the aspect-derived 1280x720 made it 400
("resolution ... not valid ... in i2v"). Add snapResolutionToken to normalise.
Add a third request family `wan` for wan*/happyhorse*: verified against a working
happyhorse-1.0-i2v call, these are DashScope/wanx-backed and use
{ input:{ prompt, media:[{type:first_frame,url}] }, parameters:{ resolution, duration, prompt_extend, watermark } },
NOT the flat input_reference shape we were sending. Update the i2v failure-advice
message accordingly (the remaining risk is wanx needing a public image URL while
we send an inline data URL; seedance accepts the inline image).

Tests: anthropic+gemini in-chat tool loops (incl. thoughtSignature round-trip),
snapResolutionToken, seedance token resolution, and the wan input.media shape.

* feat(byok): add veo video family + default to seedance-2.0-fast

veo* models go through the gateway's OpenAI→Gemini predictLongRunning shim,
which wants `seconds` as a NUMBER and only honours `size` (no aspect_ratio) —
distinct enough from the generic flat shape to warrant its own `veo` family in
the media-adapters builder. Verified against a working veo-3.1-lite call.

Also default the BYOK chat video model to doubao-seedance-2-0-fast-260128 (the
broadly-available fast i2v/t2v path) and extend the media-adapters tests to
cover the veo family routing + duration snapping (4/6/8).

* feat(byok): hide account-source model labels for AIHubMix

AIHubMix's live model fetch returns its full account catalogue, so every
picker option carried an identical "from your account" badge that added
noise without distinguishing anything. Introduce a reusable
ACCOUNT_MODEL_SOURCE_LABEL_HIDDEN set + hidesAccountModelSourceLabel()
predicate: for listed protocols the per-option source label is dropped and
the loaded-models hint shows a plain count (new settings.modelsLoadedCount
key) instead of the account wording. Other providers are unaffected; add a
protocol to the set to opt it in.

* fix(byok): close AIHubMix catalogue SSRF + share gemini image branch

Two review fixes on the AIHubMix BYOK provider:

- SSRF: the public `/api/media/providers/aihubmix/models` GET took a
  caller-controlled `baseUrl` and fetched it server-side, so
  `?baseUrl=http://169.254.169.254/` could reach a private/metadata host.
  The catalogue lives at one fixed origin, so the route now ignores any
  `baseUrl` query and hard-codes the official AIHubMix origin — no
  attacker-chooseable fetch target remains. A focused route test asserts a
  malicious `baseUrl` is ignored and only aihubmix.com is contacted.

- Gemini image parity: the media renderer always sent AIHubMix image models
  to the OpenAI `/images/generations` endpoint, which 400s for
  gemini/imagen-family models. Extract the chat tool's Gemini-native branch
  into a shared `aihubmixGeminiImageBytes` helper and route those models
  through it from both the chat tool and the Home/New Project/CLI media path,
  so the catalogue the picker exposes is fully renderable. Added a media-path
  test that asserts a gemini-family id hits the generateContent wire.

* feat(byok): hide redundant gateway preset for single-origin AIHubMix

The "gateway preset" dropdown only ever offered AIHubMix itself (plus the
generic custom-endpoint option) because AIHubMix is a fixed-origin gateway
with exactly one preset — redundant UI. Gate it behind a reusable
GATEWAY_PRESET_HIDDEN set + hidesGatewayPreset() predicate (mirroring the
existing ACCOUNT_MODEL_SOURCE_LABEL_HIDDEN pattern) so another single-origin
gateway can opt in by adding its protocol, rather than hardcoding a check.
The Base URL field still shows the resolved origin; multi-preset and
custom-endpoint-reliant protocols (azure/google/senseaudio) are unaffected.

* feat(byok): hide Base URL field too for fixed-origin AIHubMix gateway

AIHubMix resolves its Base URL automatically, so the editable field was as
redundant as the gateway preset. Generalize the just-added GATEWAY_PRESET_HIDDEN
into one semantic concept — FIXED_ORIGIN_GATEWAYS + isFixedOriginGateway() — and
drive BOTH the preset dropdown and the Base URL field off that single predicate.
Adding a fixed-origin gateway protocol now hides both surfaces in one place; the
user only supplies an API key. azure/google/senseaudio are unaffected.

* fix(byok): close AIHubMix asset-download SSRF redirect bypass

The AIHubMix asset downloads validated the literal URL via
assertExternalAssetUrl but then fetched with the default redirect:'follow',
so a validated public URL could 302 the daemon into 169.254.169.254 / RFC1918
space (and leak Bearer/APP-Code to the hop target). Add a shared
assertAndFetchExternalAsset(url, init) helper that validates and pins
redirect:'error', and route every external asset download through it:
chat video/image (byok-tools.ts) and media image/video + SenseAudio image
(media.ts). Also add the missing isLocalSameOrigin guard to the AIHubMix
catalogue route so it matches its sibling media routes.

Regression tests lock the invariant: helper-level (blocks literal metadata
host, forces redirect:'error') plus chat video/image executors asserting the
download fetch carries redirect:'error', and a cross-origin rejection test on
the catalogue route.

Also point the AIHubMix API-key console link at the referral URL.

* feat(byok): carry New Project media-model pick into BYOK conversation

The model chosen in New Project → Media was stored on project.metadata but
dropped for BYOK providers (AIHubMix): media is produced by the generate_*
chat tools whose default comes from the per-request byok*Model field, which
ProjectView only seeded from Settings. Seed those overrides from the project's
creation-time pick instead, gated to the active BYOK provider so a cross-provider
model (e.g. a SenseAudio run whose image project defaulted to gpt-image-2) can't
clobber the user's Settings default. mediaModelProviderId resolves AIHubMix's
`aihubmix-` live ids by prefix so the seed survives before the async catalogue
loads.

* refactor(byok): remove in-chat media model selector pending unified picker

Drop the in-conversation BYOK image/video/speech model dropdowns from the chat
composer. Media model selection now comes from the project's creation-time pick
(seeded into the byok*Model overrides), and a unified selection surface will
replace this later. Also removes the now-dead option hooks/imports so the
composer no longer fires AIHubMix catalogue fetches it doesn't render. The
byok* props/handlers are retained as plumbing for the future unified picker.

* fix(byok): resolve AIHubMix fixed-origin base URL so model lists load

Hiding the Base URL field for the fixed-origin AIHubMix gateway left
config.baseUrl empty, which blocked the live model-list fetch (it gates on a
valid base URL) so the Settings dropdown and the top-bar switcher showed only
the static suggested models. Centralise the fixed-origin concept in
state/apiProtocols (FIXED_ORIGIN_GATEWAYS / isFixedOriginGateway /
resolveFixedOriginBaseUrl) and resolve the canonical origin in loadConfig so
every consumer sees it — including configs already persisted with an empty
baseUrl. SettingsDialog now imports the shared helpers instead of redefining
them.
2026-06-04 09:00:20 +00:00
kokiandGitHub 10ac33097e docs(readme): add @YOMXXX to maintainers (#3633) 2026-06-04 08:56:58 +00:00
0e6d3176ab fix(web): prompt injection warning chip + subdirectory files and tab state (#2804)
* fix(web): show subdirectory files in root list and preserve design-files nav state across tabs

Two related bugs in DesignFilesPanel:

1. Since directory navigation was added (#2442), files nested in
   subdirectories were excluded from the root-level flat list, making the
   page-size select appear broken — changing it could not surface files
   the user knew existed. Fix: at the root level (currentDir === ''),
   include all files in filesAtCurrentDir so pagination covers the whole
   project. Subdirectory rows still render as navigation shortcuts.

2. Switching to any other tab unmounted DesignFilesPanel, resetting
   kindFilter, currentDir, page, and pageSize back to defaults on return.
   Fix: FileWorkspace persists the four nav-state fields in a ref and
   passes them as initial values (navState prop) plus a change callback
   (onNavStateChange) to DesignFilesPanel. The ref resets when projectId
   changes, matching the existing key={projectId} remount behaviour.

Adds a regression test for case 1.

* feat(web): add possiblePromptInjection i18n key to all locales

* feat(web): add injection variant CSS for system-reminder chip

* test(web): add failing tests for prompt injection chip

* test(web): fix null safety and strengthen assertions in injection chip tests

* fix(web): render model-echoed system-reminder as prompt injection warning chip

Add alert-triangle to Icon, update SystemReminderBlock to accept a
variant prop (trusted|injection), render injection variant with
amber warning styling and "Possible prompt injection" label, and
fix the expand test to wrap click in act() for React 18 batching.

* chore(web): document SystemReminderBlock variant semantics

* fix(daemon): prepend prompt injection resistance block to every agent system prompt

* fix(web): preserve page number across tab switches in DesignFilesPanel

Three useEffect hooks that reset page to 0 (on pageSize, kindFilter, and
currentDir changes) also fired on initial mount, overwriting the page
restored from navState. Added a skipPageResetOnMount ref — true during
the first render, set to false by an effect declared after the reset
effects — so user-initiated changes still reset the page while navState
is honoured on remount.

Adds a regression test: navigate to page 2, unmount, remount with the
captured navState, assert page 2 is restored.

* fix(web): translate possiblePromptInjection key into all 17 non-English locales

Replaces the English placeholder 'Possible prompt injection' with
localized strings so the warning chip label is readable for non-English
users.

* fix(web): reset design files nav before project switch render

* fix(web): restore prompt injection warning styling

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-06-04 08:16:51 +00:00