Commit Graph

128 Commits

Author SHA1 Message Date
wangchenglong 7d2db7014a feat(web): integrate project chat module and offline review demo
Connect the scoped chat implementation to the existing project controller, including history, message navigation, shared composer controls and the queued-message stack. Document the compatibility boundary and include the self-contained demo with client review evidence.
2026-09-10 17:29:43 +08:00
wangchenglong f7c39b9d2a feat(web): show the page taking shape while a run builds it
The Design Files pane used to sit on an empty state until a run finished
and the first artifact dropped in. Everything the run was doing happened
somewhere the user could not see, and the wait carried no signal that
anything was happening at all.

It now watches the shallowest index.html the run has produced (or the
most recently written HTML file) and renders it live beside a feed of the
steps behind it, with a toggle between the step feed and the full
preview. A new cross-runtime contract, preview-build-focus, lets the
sandboxed preview frame answer "where in this page are you writing right
now" — either by a literal string the agent just wrote or by naming one
of the page's own top-level sections — so the host can walk a cursor down
the page as it is written instead of jumping to wherever the last run of
text happens to land. The frame has no allow-same-origin, so it posts to
'*' and the host identifies it by event.source.

latest-user-prompt goes away with the empty state that echoed it: the
pane now names what the run is doing, which is the more useful of the
two, and keeping both meant recomputing a prompt string on every streamed
token for a line nobody read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 15:43:24 +08:00
wangchenglong ab0a1a4e61 feat(web): refresh the home and entry surfaces
Reworks the entry experience end to end — the Home hero and its composer, the
nav rail, the workspace tab bar, the recent-projects strip, the community
gallery and the message center — and adds the per-project run status the rail
and the strip now render.

Home / entry
- Hero headline cycles a noun out of `homeHero.titleWords` (RotatingTitleWord);
  locales whose `homeHero.title` carries no `{word}` stay a plain sentence.
- The composer is now one component in two shapes: `page` (Home's full column)
  and `dock` (the bar alone, used by the community view), sharing @mentions,
  staged files, the working-directory row and submit.
- ComposerModePicker is gone; ComposerPlusMenu absorbs what it offered.
- Entry layout, nav rail (with RailRecentRow), workspace tabs and the recent
  projects strip are restyled together against one set of measurements.

Run status
- New `projectRunStatus` state, `useProjectRunStatuses`, `run-progress` and
  `latest-user-prompt` runtime helpers, and `ProjectRunStatusIcon`.
- `ChatRunsResponse` gains `awaitingInputProjectIds`: a run that asked a
  question reads `succeeded` while the project is still blocked, so a caller
  rendering per-project status needs the set to avoid showing such a project
  as finished. Optional — absent means unknown, not none.

Community / message center / design files
- Community gallery gets its own empty state, and the message center gets
  archive plus a reworked card.
- Design files panel gets a dedicated empty state component.

Daemon / desktop / sidecar
- Project create bounds its read-only preparation work (15s) so a stalled
  local catalogue scan cannot strand the web on an optimistic route or leave a
  half-initialized project behind.
- The startup splash runs the same pixel-scan wordmark as the Home hero,
  ported to raw WebGL because the splash renders before the sidecar boots
  (replaces the splash video).
- Sidecar proxy tracks the in-flight request/response so a keepalive replay
  tears down the previous attempt instead of leaking it.

New visuals: SiriOrb, MarqueeLabel, SpaceBackground, the composer beam and the
chat-cards-next surface.

No placeholder data ships with this: the community card byline (author name and
view/remix counts) and the message center's temporary cover image are removed,
so the card footer shows the title only and the media slot renders only when a
message payload actually carries `imageUrl`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016cbrF36bYyw9qpnksvZTjd
2026-08-31 12:01:27 +08:00
lefarcen 34ad192de9 fix(web): recover aborted srcdoc preview navigation (#6882)
* fix(web): recover aborted srcdoc preview navigation

* fix(web): close srcdoc recovery races

* fix(web): version srcdoc base navigation
2026-08-14 08:02:26 +00:00
open-design-crew[bot] 7a37c1462d feat(export): add reliable standalone HTML export (#6855)
* feat(export): add standalone HTML export

Bundle project-local HTML, CSS, JavaScript, images, fonts, workers,
and iframe dependencies into one offline-safe file.

Expose the same contract through the web download flow and od CLI,
with structured failures and bounded output assembly.

* fix(daemon): honor standalone source URL semantics

Resolve document assets against local base href values, reject browser-invalid TypeScript and JSX sources with a structured 422, and count unquoted first-level resources before graph traversal.

Validated with daemon tests, workspace typecheck, and guard.

* fix(daemon): handle standalone data scripts and CSS URL tokens

Preserve non-JavaScript script payloads as data blocks and scan CSS values without treating strings or comments as project dependencies.

Validated with focused and full daemon tests, workspace typecheck, and guard.

* fix(daemon): classify standalone export resources correctly

Preserve non-HTML iframe MIME types, inline only resource-bearing link relations, and propagate unexpected Vite dist read failures instead of silently falling back.

Validated with focused export tests, the full daemon suite, guard, and workspace typecheck.

* fix(export): preserve version and stylesheet semantics

Hide standalone HTML export for historical versions until dependency snapshots are available, avoiding an action that always fails.

Keep disabled and alternate stylesheets as embedded link elements so standalone export preserves activation semantics. Validated with focused red/green coverage, the full web suite, pnpm guard, and pnpm typecheck.

---------

Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
2026-08-14 07:21:21 +00:00
lefarcen 9a402b2e4c feat(campaign): extend the DeepSeek V4 campaign to Pro + Flash (#6861)
* feat(campaign): extend the DeepSeek V4 campaign to Pro + Flash

The V4 Flash free week ends 8/13 20:00; a two-week window offering BOTH
V4 Pro and V4 Flash opens at that same instant and runs to 8/27 20:00.

This grows the campaign already in production rather than landing a
second stack beside it. That implementation ran the free week for a
week: its audience resolution (wallet balance is not a subscription),
i18n wiring, localized countdown, frequency control and attribution are
all already exercised, and it carries a product decision this change
must not undo -- the URL review backdoors (campaign / audience / usage
overrides) were deliberately removed, and reviewing the campaign is done
by overriding `window.startAt`, never through query parameters.

Almost all of it is the campaign module growing a second model:
`isDeepSeekV4FlashCampaignModel` now matches either id, which is why the
model switcher badges both without its rendering changing at all, and
the modal's benefit line is an i18n key, so naming both models was copy.
`「立即使用」` now switches a paid user to Pro, the headline benefit.

Two things needed real edits. Model-benefit impressions fired once with
a hardcoded Flash id; product compares the two models' reach separately,
so they now fire once per campaign model actually on screen, each with
its own `model_id`. And `campaign_id` / `element` move to the spec's
`deepseek_v4_pro` naming, with `deepseek_v4_flash` kept declared so the
finished campaign's warehouse rows stay valid.

Copy is rewritten across all 19 locales -- dates, the two-week framing,
both model names, and the peak-hours queue caveat product added. Dates
follow each locale's existing format rather than one imposed format, so
only the day numbers move.

Frequency control is keyed on the campaign id, and the id is new, so a
user who dismissed the free week is still owed this campaign's single
showing. That was an invisible dependency on remembering to change the
id; it now has a test.

Module and i18n key names keep their `…V4Flash` spelling: renaming 28
keys across 19 locales mid-campaign is risk without user-visible
benefit, and belongs in a cleanup pass once the campaign is off.

* fix(web): read a rolling model window as a wait, not a failure

vela's hosted gateway rejects a request that exhausted its rolling
per-model window with `model_limit_exceeded`, phrased for API callers as
"You have reached the 5-hour usage limit for X. Try again after <instant>.
This request was not charged to Wallet Credits."

Two things went wrong with that sentence on the way to a user.

The daemon's `isHardQuotaText` matches its "usage limit" wording, so a
window that resets on its own was classified as `hard_quota` — an
exhausted quota, marked non-retryable. Nothing was charged and nothing
needs topping up, so that both misnames the cause and counts a self-
healing wait against reliability as a real failure.

The web runtime then had nowhere to put it. The AMR branch of
`resolveRunFailureUi` ends in a catch-all returning the generic "task
failed" title with a null message key, which renders the raw upstream
string — so a localized chat showed an English sentence under a wrong
heading. Home is worse: its send path fails before a run exists, never
reaches that mapping at all, and prints `err.message` verbatim.

Classify the window separately, ahead of the hard-quota reading, and give
both surfaces the same localized copy: high demand, when it reopens, and
the reassurance that nothing was charged. The instant is rendered in the
reader's locale — the gateway reports UTC. Both surfaces read the signal
through one shared contracts module so they cannot drift, and degrade to
copy that does not promise a time when no instant is readable.

Other rate limits are untouched: a genuine session-limit exhaustion keeps
its hard_quota reading, and every other business failure on Home still
prints verbatim.

* fix(analytics): keep a campaign entry for the whole campaign

The attribution record expires after seven days, but this campaign runs
fourteen. A visitor who clicks the banner on day 1 and subscribes on day
11 arrives at checkout with no entry left to attribute the payment to,
so the campaign under-reports against the metric it is judged on --
活动归因付费人数 / 金额, both defined as payments carrying its
`campaign_id`.

Extended only for entries that carry a `campaignId`. Raising the window
globally would move the attribution口径 for every other entry point and
the dashboards already built on it; the campaign is the thing that needs
a longer memory, so it is the thing that gets one. Past fourteen days a
campaign entry expires like any other -- the longer window is scoped to
the campaign, not an open-ended exemption.

* fix(campaign): carry the new campaign id through ingest and e2e fixtures

Two places still recognised only the finished free week's id, both found
in review.

The daemon's AMR entry ingest allowlist is fail-closed: an unrecognised
`campaignId` voids the WHOLE entry rather than just its campaign field.
With only `deepseek_v4_flash` listed, every entry this campaign produces
would have been dropped at ingest -- and its success metrics are defined
as payments carrying its `campaign_id`, so it would have reported zero
conversions while converting normally. Both ids are listed now, since
entries minted during the free week can still arrive inside their
attribution window.

The Playwright suite pre-seeds a dismissal so the campaign modal never
opens over a functional spec. That seeding was keyed on the old campaign
id, so the modal would have covered Home in every UI spec. `home-hero-
rail` also clears storage and re-seeded its own hardcoded copy of the
key; it now reapplies the shared map instead, so a future campaign
cannot be dismissed in one place and left interrupting specs in the
other.

* fix(campaign): land the workbench badge on the console, like the modal

The badge opened a hardcoded `https://open-design.ai/zh/pricing/` while
the modal's CTA opened the console's plan surface scoped to the caller's
workspace. Both are in-product entry points for a signed-in user, so one
funnel was being split across two destinations -- and only one of them
lets the user actually subscribe.

The hardcoded link also pinned `/zh/`, so every non-Chinese user landed
on a Chinese page from a badge that renders in all 19 locales.

Now shares the modal's destination, including the workspace scope, so
the console can resolve the right upgrade surface for that workspace.
Verified against a real browser: the badge opens
`/amr/dashboard?workspaceId=…&billing=plan` carrying entry id, source,
campaign id, conversion source and device id.
2026-08-13 16:37:16 +00:00
Kiuber 91f34d66e9 fix web powered preview behind reverse proxies (#5592) 2026-08-13 07:49:02 +00:00
lefarcen b55d443b44 fix(web): streamline Home submit preflight (#6756)
* fix(web): retry transient home balance preflight

* refactor(web): streamline home submit preflight

* fix(web): keep local submit independent from cloud identity

* fix(web): preserve local and legacy submit lanes

* fix(web): keep in-memory workspace witness valid

* fix(web): retain workspace preflight compatibility

* fix(web): require workspace witness for team plugin apply

* fix(plugins): apply exact local plugin source

* fix(web): invalidate legacy witness on identity change

* refactor: keep Home submit local-first

* test: align local-first creation coverage

* fix: preserve exact local plugin context

* fix: fence exact local plugin use

* fix: disable inexact legacy plugin fallback

* test: mock exact plugin apply in visuals

* fix: reject unsupported exact project source

* Revert "fix: reject unsupported exact project source"

This reverts commit 99e0cf23c1.

* Revert "test: mock exact plugin apply in visuals"

This reverts commit 33b36688c7.

* Revert "fix: disable inexact legacy plugin fallback"

This reverts commit d1b4d9bcaa.

* Revert "fix: fence exact local plugin use"

This reverts commit aebaf6a20b.

* Revert "fix: preserve exact local plugin context"

This reverts commit aa6ab67631.

* Revert "fix(plugins): apply exact local plugin source"

This reverts commit e776a7d079.

* fix: reconcile local submit selections

* fix: preserve cold cloud submit handoff

* test: seed active team design-system binding

* fix(plugins): preserve exact local source semantics

* test(web): align Home failure copy after main merge

* fix(home): preserve staged resource catalog scope
2026-08-13 06:33:29 +00:00
lefarcen e44bd3f907 fix(web): recover stalled iframe preview refreshes (#6714) 2026-08-10 15:19:24 +00:00
lefarcen 1ca8620e9f feat(web): report preview iframe failures to PostHog (#6671)
* feat(web): report preview iframe failures

* fix(web): preserve preview error stacks

* fix(preview): scope and normalize observability bridge

* fix(preview): ignore retained iframe diagnostics

* fix(preview): consume early diagnostics once
2026-08-10 06:08:40 +00:00
YOMXXX 4b720a0045 fix(web): stop replaying stale succeeded Design runs on reload (#6578)
* fix(web): stop replaying stale succeeded Design runs on reload

A Design-mode assistant message can be persisted as `succeeded` with its
delivery metadata (`resultDeliveryState`/`producedFiles`/`traceObjectFiles`)
absent — historical rows created before a final project-file refresh, or
after an interrupted persistence path. `designDeliveryVerificationPending`
has no age bound, so `shouldReplayTerminalRunMessage` returns true for such
rows forever: every reload re-enters the reattach path, and if the SSE
replay drops, `onError` reverts the message to `succeeded` without sealing
`completedReattachRunsRef`, looping indefinitely.

Bound the reconciliation window: a succeeded Design row whose `endedAt` is
older than DESIGN_DELIVERY_RECONCILIATION_WINDOW_MS (5 min) is no longer
auto-replayed — it renders as a terminal outcome instead. Freshly completed
runs (or rows without a terminal timestamp) keep the existing one-shot
reconciliation.

* fix(web): bound Design replay reconciliation from startedAt/createdAt too

Addresses #6578 review feedback:

- Bound the #6505 reconciliation age from any persisted terminal
  timestamp (`endedAt` then `startedAt` then `createdAt`) so legacy rows
  persisted before `endedAt` existed are also flagged stale and stop
  auto-replaying on reload.
- Widen the `designDeliveryReconciliationStale` input type to include
  `startedAt`/`createdAt` so the legacy-shape fixtures typecheck.
- Update synthetic terminal timestamps in tests to realistic values so the
  real-clock age bound does not read them as years old; add a regression for
  the no-`endedAt` legacy shape.
2026-08-07 09:32:39 +00:00
YOMXXX 04c6af5b57 fix(web): count unique produced files in "Files from this turn" header (#6420)
* fix(web): count unique produced files in "Files from this turn" header

The disclosure header summed per-op tool_use counts, so a file written
or edited several times inflated the total (e.g. "Write 9" for 6 files,
"Edit 4" for 2 edited files) — mismatching both the titled semantics
("Files from this turn") and the unique-file rows below it (#5909).

Count each unique file once in the header, categorized by its primary
artifact op (edit > write), matching how each row's badge is chosen.
The op-level counter (countFileOps) is unchanged for other callers.

Red spec: flip the existing FileOpsSummary component assertion from the
op-level "Edit 4" to the file-level "Edit 2", plus a unit test for the
new countArtifactFileOps helper.

* test(web): cover the write-then-edit path for the artifact file count

The countArtifactFileOps case only exercised a read+edit file for the
`edit > write` rule. The #5909 repro path is create + edit the SAME file,
so switch the b.ts entry to write-then-edit and assert it still counts as
one edit (not one write + one edit), while the op-level counter sees the
additional write (roian6 / lefarcen review on #6420).
2026-08-07 08:43:43 +00:00
Nicholas-Xiong 4de25d3f1b fix(web): 让长演讲者备注在 presenter 视图里滚动而非裁切 (#6271) (#6314)
问题:.presenter 视图里 .notes 这个 aside 作为 body 的 grid item 默认
min-height: auto,不收缩;body 的隐式 grid row 也 auto,跟着内容撑。
当备注文字到几百词时,.notes 把 body row 撑过 viewport,而 body
{ overflow: hidden } 直接切掉底部,.notes-body { overflow: auto }
永远不触发 —— 用户看不到下面的备注,也没有滚动条 (#6271)。

修复:
1. body 加 grid-template-rows: minmax(0, 1fr),把 implicit row 锁到
   viewport 高度,row 不再随内容增长。
2. .notes 加 min-height: 0,让 grid item 在 bounded row 里可以收缩,
   .notes-body { overflow: auto } 才能真正生效。这跟 .stage 已有的
   min-height: 0 模式对称。

回归测试:在 speaker-notes.test.ts 加一个 describe,80 行 ~800 词的
备注塞进 buildSpeakerNotesPresenterHtml,断言:
- body 的 CSS 里包含 grid-template-rows: minmax(0, 1fr)
- .notes 的 CSS 里包含 min-height: 0
- .notes-body 仍然带 overflow: auto
- 完整长备注文本被原样写入 od-presenter-data
锁住这几个不变量,后续 refactor 不会再让长备注被静默裁切。

Co-authored-by: Ray Xi <2667192167@qq.com>
2026-08-07 07:54:30 +00:00
lefarcen 356c8c364f feat(workspace-team): team workspaces, shared resources, workspace-scoped billing, and the #5517 redesign (#6142)
* fix(web): scope plugin URL imports to workspace

* test(web): cover billing interest remount lifecycle

* feat(packaged): enable the workspace-team transport for test-profile builds

The vela test backend now serves the workspace-team API, so a packaged build
baked with the test AMR profile should drive team projects / collab / resource
sharing there instead of leaving them dormant. Replace the single feature-test
equality check with a profile -> vela web origin map so adding a backend is one
entry, and keep every unlisted profile (prod above all) off.

* test(daemon): cover billing catch-up after SSE reconnect

* fix(collab): surface and start first materialization when opening a shared project

QA P0: a brand-new member on a fresh install joins someone's workspace, opens a
project from it, and sees no loading state and no files — content only appears
on a SECOND open, much later.

Two defects, both on the first `/collab/status` request that opening a shared
project makes.

1. No pull is started. The daemon's self-materialization block was gated on
   `callerIsOwner`, so a non-owner member got the placeholder record registered
   and nothing else. The web's auto-pull is gated on `publishedVersion`
   advancing past its cursor, and a fresh daemon's first status response cannot
   carry a published head: `collab.publishedVersion()` reads an in-process map
   that has never been written, and the real hub head is fetched
   fire-and-forget into `headEnrichmentCache` for a LATER poll to consume.
   Materialization therefore arrived only whenever a proactive lane (hub push /
   reconnect catch-up / the 30s recovery floor) next fired — the "second open"
   the report describes.

2. No download state is shown. `useProjectCollab.downloadPending` — which picks
   DesignFilesPanel's "Syncing files from the team…" over its empty state and
   create-a-file CTAs — reads only `publishedVersion > cursor`,
   `contentTransferState`, and an in-flight pull. All three are blank on that
   first response, so the member was shown an empty project inviting them to
   start creating over content that was still on its way.

The fix makes the placeholder stamp the load-bearing signal on both sides:

- `materializePlaceholderOnOpen` (routes/collab-sync.ts) replaces the
  owner-only block: any viewer opening a shared project whose only local record
  is an unmaterialized placeholder starts its pull on that same request,
  through the same coalesced flow. The retracted-share heal in its catch stays
  owner-only — a member who hits the hub tombstone has merely lost access and
  must not unshare anyone's project.
- `/collab/status` reports `awaitingFirstMaterialization`, a purely local,
  synchronous fact that needs no hub round-trip and is therefore available on
  the very first response. `od collab status` prints it too.
- `localFilesAreNotTheContentYet` (useProjectCollab.ts) makes it always
  download-pending, bypassing the member-only `shouldAutoPull` gate: whether the
  local files are the content is a fact about the record, not about who may
  pull it, so the owner of an unmaterialized placeholder (the reinstall case,
  recvqzaDvUU6B3) stops seeing the same empty state too.

Red evidence: all six new specs fail on origin/feat/workspace-team @ 793d7b92c
— the first status response reported `awaitingFirstMaterialization: undefined`
with publishedVersion/materializedVersion/contentTransferState all null,
`beginContentTransfer` was never called for a member's first open, and
`downloadPending` was false for both the member and the owner.

* chore(packaged): inject the vela web origin at build time, not source

This repository is public. Commit 319cb42b3 added a profile -> vela web
origin table to apps/packaged/src/sidecars.ts, which put an internal
environment's hostname into product source and would have published it on
merge. The same hostname had also reached apps/web/src/runtime/amr-guidance.ts
as a new profile row.

Internal AMR environments are not public (one of them fronts a Stripe
sandbox), so their origins now travel the same build-time path POSTHOG_KEY
already uses:

  release-beta.yml env (per-profile secret)
    -> tools-pack config (OD_VELA_WEB_URL)
    -> open-design-config.json (velaWebUrl)
    -> packaged config -> daemon spawn env (OD_VELA_WEB_URL)
    -> GET /api/integrations/vela/status (consoleOrigin)
    -> web runtime console links

The workspace-team gate keeps its meaning and gets stricter: it now needs an
allowlisted AMR profile AND an injected origin. `prod` can never satisfy the
profile half, so a stable build stays dormant however it is configured, and a
feature-test/test build whose secret is unset stays dormant too rather than
pointing the transports at an unknown backend.

A build with no secret configured therefore ships with workspace-team silently
off — the same failure mode as a fork build without POSTHOG_KEY, and the safe
direction for an unreleased feature.

apps/packaged/tests/source-origins.test.ts is a standing guard: every absolute
URL literal under apps/packaged/src must resolve to a publishable host, so the
next backend origin cannot be hardcoded there either.

Secrets required for a workspace-team dogfood build:
VELA_WEB_URL_FEATURE_TEST, VELA_WEB_URL_TEST.

* test(e2e): follow the #5517 plugin-details route in the visual capture

`captures the home plugin use staged surface` has been failing on this
branch since before the main merge. It was written against the pre-#5517
UI: clicking a marketplace card opened a role="dialog" details overlay
with a per-slug `plugin-details-use-<id>` action.

#5517 made plugin details a full-page route — `openCardDetail` calls
navigate({ kind: 'marketplace-detail' }) for plugin records, rendering
`PluginDetailView` at /marketplace/<id> whose Use control is a single
`plugin-detail-use` button. Per the product rule that the accepted
feature-branch UI is the source of truth, the test follows the route
instead of the UI being changed back.

Asserting the route alone was not enough: the detail surface refetches
the record from /api/plugins/<id>, which the visual fixture never mocked
(it mocks the list, */preview and */apply), so the route rendered its
"Failed to load plugin: HTTP 404" branch. configureVisualPage now serves
the single-plugin GET from the same VISUAL_PLUGINS fixture.

The capture asserts the Use button rather than the `plugin-detail` shell
because the shell also renders for the loading and load-failed states.

* fix(packaged): restore masked CLI icons (#6154)

* Sync local workspace-team batch: UI polish, popover simplification, update-reminder progress (#6156)

* UI polish batch: import-dialog cleanup, settings toast placement, tab chrome fixes

- Plugin/skill import dialog: drop per-card icons, left-align card content
- Settings page: center the autosave pill under the top nav
- Avatar model menu: soften the pinned footer divider
- Workspace tabs: pinned entry tab always reads as Home; remove tab-search
  button and its popover
- Model picker: wider row gap in the two-pane browse popover
- Community header: unpin (scrolls with content)
- New update-reminder dialog assets and strings
- Keep entry-settings-button as the signed-out settings entry (e2e contract);
  align NextStepActions with the navPlugins key rename

* Update reminder: simulate in-place download progress instead of linking out

The strip's confirm now runs a staged progress state ('updating') with an
eased fake download bar, then marks the version updated — replacing the
jump to the GitHub releases page.

* test(web): retire AvatarMenu account-row suites after the popover simplification

The composer popover was reduced to a model picker (2026-07-24): the Open
Design account row — plan badge, balance, wallet fallback, upgrade/console
links — no longer renders there, so the nine suites asserting that surface
(including the workspace-balance and billing-permission gates that landed
upstream meanwhile) can no longer pass. Drop them and recast the remaining
account-row test as the guard for the new invariant: a signed-in AMR status
must render no account UI in the popover.

Also drop the stale onClose prop from the new upstream sign-out-confirm
test; the rail no longer takes one.

* Design-systems toast: anchor to the pane's top edge, pill radius

* Update reminder strip: progress ring on the button, percent face

While updating, the collapsed strip no longer stacks a label + bar above
the rocket button. Progress draws as a brand-green ring hugging the round
button's edge (5px band reserved so the button never shifts), and the
button face swaps the rocket for the bare percent — 'N%' while
downloading, 完成 (updater.done) at 100. The full 正在下载更新 N% label
stays reachable via the hover bubble and the ring's aria-label. Demo
payload version bumps to 1.4.6 so the once-per-version card re-arms.

* fix(web): restore design kit section spacing (#6155)

* fix(web): carry the plugin back to Home when Use runs from the detail route

Product call: using a plugin from the full-page detail route should land on
Home with that plugin already selected and its brief pre-filled, exactly
like the marketplace card's "Try it".

It did not. `PluginDetailView.onUse` applied the plugin, kept the result in
local state, and navigated home. `App` renders that route outside
`EntryShell`, so the navigation unmounted the holder and dropped both the
plugin and the brief — Home came up empty. The "redirected to Home with the
brief pre-filled" note under the button could never be seen either, since
the navigation fired immediately; it is removed rather than left lying.

Reuses the existing channel instead of adding a parallel one: onUse now
publishes the same `createPluginUseHandoff` payload "Try it" produces, and
Home's existing pendingPluginUseHandoff path applies it unchanged. The
handoff is parked in module scope — not on `window` — so it survives the
unmount, and `EntryShell` claims it in a lazy state initializer so it
arrives in the same commit as the mount. Reads are destructive, so a
handoff applies once and does not re-fire on the next visit to Home.

Red first: PluginDetailView.use-handoff.test.tsx fails on the unfixed code
at the handoff assertion (navigation and the failure path already passed),
and goes green with this change. The e2e visual capture that asserts
home-hero-active-plugin should follow without weakening its assertion.

test(e2e): match the settings surface in both #5517 presentations

openSettingsDetailsFromHeader still waited on `.modal-settings[role="dialog"]`.
#5517 routes the entry's settings to /settings, where SettingsDialog renders
in presentation="page" mode with `role="region"`, so that selector never
matched the page presentation: an already-open settings surface looked
absent and the helper fell through to hunting for entry triggers that
surface does not carry, failing 16 captures on one line.

Reuses `settingsSurface` from amr.ts — the existing bare `.modal-settings`
matcher both presentations share — rather than adding a third local
variant. That helper also documents why the `role="dialog"` fallback was
actively wrong: AvatarMenu is a dialog too, so it could resolve to the
account menu.

* test(e2e): open the nav rail before reaching for entry settings

The bare-`.modal-settings` change alone did not move the settings-workspace
lane: all 16 captures still died on `.settings-icon-btn`, because the
failure happens before the surface selector matters. None of the four
triggers was ever visible.

amr.ts already documents why. #5517 moved the entry settings chip into the
nav rail footer, and a collapsed rail is `inert` + `aria-hidden`: the chip
is present but invisible to `getByRole`, and even a programmatic
`element.click()` is a no-op. `openSettingsDialog` expands the rail first
(`ensureEntryRailOpenIfPresent`), clears loading, dismisses the privacy
dialog, matches the surface with the shared bare class, and ends its
trigger chain on the settings aria-label instead of `.settings-icon-btn`,
which the entry surface does not carry. It also clicks
`entry-settings-open-details`, so it is a superset of the local copy.

So `openSettingsDetailsFromHeader` now delegates to it and keeps only its
name, leaving exactly one settings opener instead of a third variant.

* test: realign the suites #6156 outpaced, and reach Settings from a project

#6156 (4e3161751) landed a UI batch whose test fallout was only partly
handled. Four Vitest files and four e2e call sites still describe the surface
as it looked before it, and the Playwright settings opener never had a trigger
that exists once a project is open.

Vitest — the assertions now read the surface that shipped:

  - community-view: the type tabs traded their `<small>` count badges for type
    icons, so `readFacets().badge` was `NaN`. Counts move to a new
    `readFacetCardCounts()` that drives each tab and counts the grid — the same
    array the badges were derived from, so the catalogue-fidelity guard
    (including "never grids more than the whole catalogue") survives the
    badge's removal rather than being dropped.
  - home-logo-assets: the rail's signed-out brand header is gone (with no cloud
    identity the rail starts at the search box), so it can no longer carry
    `od-brand-glyph`. Keeps the guard that still means something: it must never
    fall back to the retired raster app icon.
  - acceptance-visual-fixes: recvq4iEq1Esno's fix is intact — page mode still
    hides only `.settings-chrome-btn`, leaving the autosave pill visible. #6156
    re-added the bare `.settings-page-shell .settings-chrome` selector purely to
    centre that pill under the top nav, so assert the invariant the row is
    about (no page-mode `display: none`) instead of the selector's absence.
  - FileViewer: the new `artifact-preview-first-load` cover is a `role="status"`
    of its own, so `findByRole('status')` had become ambiguous. Scope the
    success assertion to the toast.

e2e — `entry-nav-settings` no longer exists anywhere in `apps/web/src`; the
rail item carries `entry-settings-button`. #6156 renamed it and updated
entry-chrome-flows' two `toHaveCount(0)` assertions but not the four places
that click or await it (critical-smoke, entry-topbar x2, entry-chrome-flows x2).

`openSettingsDialog` also only ever knew entry-shell triggers, all of which
live on Home. From a project every one of them is absent — and
`EntrySettingsMenu` and `AppChromeHeader`'s `SettingsIconButton` are both
unrendered — so the surface never opened and the failure surfaced as a missing
`.modal-settings`. Add the project surface's real entry: the composer model
popover's pinned `avatar-open-execution-settings`, with the topbar switcher's
`inline-model-switcher-open-settings` as a second route.

Not addressed here, deliberately: #6156 also deleted the `settings-general-field`
block that rendered `AppearanceSection`, orphaning the component. That removes
the product's only System/Light/Dark control (the rail's account menu dropped its
theme row *because* Settings owned it), so the 11 `SettingsDialog.execution`
appearance failures are reporting a capability loss, not a stale expectation.
Restoring it is a product call, so those tests are left red rather than weakened.

* test(e2e): finish the popover the settings chain opens on a project surface

`openSettingsDialog`'s trigger chain ends on OPEN_SETTINGS_LABEL, which matches
`Account & settings` — and that is `avatar.title`, the label on AvatarMenu's
own trigger. So on a project surface the chain does find something and clicks
it, but what opens is the composer's model popover, not Settings. The
follow-through step then looked only for `entry-settings-open-details`, which
#5517 left unrendered, so the run gave up with a missing `.modal-settings`
after three identical attempts.

Widen the follow-through to the two rows that actually route to Settings from
a project — AvatarMenu's pinned `avatar-open-execution-settings` and the topbar
switcher's `inline-model-switcher-open-settings` — so whichever popover the
first click opened gets finished.

* test(e2e): pin the AMR selection assertion to the agent card

With Settings actually opening from a project surface again, the surface-wide
`/Open Design/i` in amr-logout-requires-relogin resolves to the sidebar's
"Open Design MCP" `settings-nav-item` — which carries no `aria-pressed` — not
to the AMR agent card's select button. The looseness was invisible while the
surface never opened.

Read the toggle through `settings-agent-card-amr` instead, so the assertion
names the thing the test is about: AMR stays the selected agent.

* test(e2e): prove Settings opened via its section nav, not a heading probe

`prepareVisualSettingsDialog` gated on a heading matching
/Settings|General|Execution mode/. Now that the surface opens from a project it
lands on the execution section, whose heading reads "Models & providers" — and
the surface's own <h2> is consumed as its accessible name via aria-labelledby
anyway, so the probe could not match in either presentation.

Assert `settings-nav-execution` instead, which is what critical-smoke already
uses for the same purpose and holds for both the modal and the routed page.

* test(web): retire the theme/appearance cases the product cut

Product decision (2026-07-28), verbatim: 「主题设置不要了,因为 workspace 功能不
支持暗色主题,要干掉,并且之前用户如果设置了暗色主体,需要强制改成明亮色主题」

So #6156 deleting the `AppearanceSection` render site was the right direction,
and the NON-ALIGNMENT #9 note that argued for keeping the segmented control as
the product's last "follow system" entry point is formally overturned. These
cases are retired because the capability is gone by decision — NOT to turn CI
green. The source-side removal (orphaned `AppearanceSection`, onboarding
light/dark toggle, `settings.appearance*` keys, `trackSettingsAppearanceClick`,
plus forcing stored dark/system back to light for existing users) lands
separately on `fix/remove-theme-force-light`.

Deleted (8) — their whole subject was theme or the accent it carried:
  - offers the theme segmented control, and System leaves the document theme unset
  - applies the stored default accent color even though the picker is gone
  - writes the picked theme to the document and autosaves it
  - live previews the configured theme on open, and System leaves no explicit
    document theme
  - reverts an unsaved appearance preview back to the saved appearance when the
    dialog closes
  - persists System mode explicitly and preserves accent variables without an
    explicit document theme
  - keeps a stored non-default accent applied and carries it through an autosave
  - localizes the theme controls in Chinese

Nothing was left behind as a loose "still renders something" assertion, and no
live coverage went with them: the close-button/`onClose` path the revert case
also touched is already covered at five other call sites in this file.

Kept (3) — theme was only the vehicle, the subject is still shipping. Each now
rides the notifications completion-sound toggle, seeded on so the single click
is a real state change (the pills no-op when clicked in their current state):
  - reconciles the open settings draft when the parent agent CLI env changes —
    the only integration proof that `reconcileAmrModelChoice` /
    `reconcileAmrProfileEnv` actually run through the real draft on autosave;
    its two siblings are pure unit tests and do not cover that path.
  - drops a pending autosave when explicit onboarding reset unmounts Settings —
    subject is the pending-autosave drop and the draft the reset carries.
  - still autosaves an unrelated edit that lands during a silent-update save —
    subject is autosave bookkeeping (success must only advance
    autosaveLastSavedRef for allowSilentUpdates).

The enclosing block is renamed 'SettingsDialog draft reconciliation' since it no
longer describes appearance, and its document theme/accent teardown is dropped —
nothing in it writes those any more. 144/144 green.

* test(e2e): realign the visual workspace captures with the narrowed surfaces

Three deliberate narrowings shipped without this Playwright lane following, so
eight captures were driving UI that no longer renders. All eight are stale
fixtures; none was a regression.

  - `ef9c8cd8b` made the home top-bar `InlineModelSwitcher` `compact`, and
    `EntryShell` (its only call site) always passes the flag. In compact mode the
    popover is just the active agent's model radio list plus the route to
    設定 → 執行; the mode segmented control, agent grid, account block, and both
    searchable dropdowns live in the non-compact branches. `68cecac1c` records
    that product confirmed this narrower shape, so acceptance #40 ("home 页不能
    切换 cli 了") is working as designed.
  - `4e3161751` (#6156) removed the Open Design account row from `AvatarMenu`
    (plan badge, balance, wallet fallback, upgrade/console links) and retired the
    nine equivalent Vitest suites — but not these.
  - `56b538aa4` moved the design-system picker out of the staged-context bar, so
    `staged-contexts` is no longer unconditionally mounted.

Where a surface still ships, the capture follows it and the assertion got
sharper rather than looser: staged contexts now stages a real attachment and
names the chip; the topbar/avatar captures assert the surviving model picker
plus the *inverse* invariant (no account block, no upgrade button, no balance),
gated on a counted `vela/status` request so a negative cannot pass by racing the
fetch. `:57` additionally pins `inline-model-switcher-mode-daemon` at count 0, so
re-mounting the CLI console on Home fails here again.

`:167` (topbar BYOK model dropdown) is deleted: in compact mode it would
re-capture the identical popover as `:141`, which is a duplicate screenshot
rather than coverage. `:141` absorbed its guard.

`:244` (avatar reasoning readout) turned out never to have passed — not a #6156
casualty. `VISUAL_CLI_AGENTS`' codex entry declares `models` only, while
`AvatarMenu` draws the readout solely for agents reporting `reasoningOptions`, so
it has been red since `68cecac1c` authored it. Fixed by declaring
`reasoningOptions` on a test-local codex fixture, matching what the real daemon
reports (`apps/daemon/src/runtimes/defs/codex.ts:164`).

Nothing was dropped without a verified live home elsewhere in the same CI lane
or in Vitest: the AMR plan/balance/upgrade card at `visual-settings.test.ts:30`,
the searchable dropdowns at `visual-settings.test.ts:93`/`:156`, the upgrade
attribution URLs in `InlineModelSwitcher.test.tsx:585` and
`AvatarMenu.test.tsx:283`, the "no account row" invariant at
`AvatarMenu.test.tsx:528`, and real staged-context chips in
`project-management-flows.test.ts`. The one assertion with no surviving home is
the Open-Design-first agent ordering, whose grid no longer renders anywhere;
that is called out in the test comment rather than silently lost.

* test(e2e): give the AMR upgrade capture a billing-capable workspace

`settings-agent-card-amr-upgrade` stopped rendering for this capture because
`amrCardCanUpgrade` (SettingsDialog.tsx:4651) now also requires
`workspaceContext?.permissions?.canManageBilling` — this branch's
workspace-scoped billing work, i.e. the feature #6142 exists to ship. The gate
is correct; the fixture was not representing anyone entitled to pass it, since
`mockSignedInVelaAccount` establishes a Vela session but no workspace context.

Stub `/api/workspace/context` with a personal-owner context inline in this one
test rather than widening `mockSignedInVelaAccount`, which visual-workspace
shares. The assertion is unchanged — the upgrade entry is a real capability for
billing-capable members, so the fixture represents one instead of the assertion
being relaxed.

Verified the stub does not quietly satisfy the three assertions that already
passed: `planId: null` keeps `resolvePlanTier` falling through to `plus`, and
billing still 404s so the balance stays `$247.51`.

Expect one legitimate baseline diff on `visual-settings-open-design-account`:
the rail now renders signed-in workspace chrome, which is the only state in
which the upgrade entry exists at all.

* test(e2e): leave projects through shipping chrome, and reach /projects reliably

Two dead locators and one under-budgeted wait, all test-side.

`openNewProjectModal` (rail.ts) probed `entry-nav-new-project` then
`entry-nav-projects`, both deleted from EntryNavRail by the #5517 redesign
(`b55f17169`, `f16075f7e`) — `onNewProject` is still destructured there but has
zero call sites. Its body is byte-identical to main, where both testids exist, so
main takes the rail path and never reaches the fallback this branch always hits.
The live affordance is `designs-new-project` / `designs-empty-new-project` in
DesignsTab, inside `entry-view-projects`; `/projects` has no UI entry at all
(entry-chrome-flows:373-382 already documents that and routes directly).

The fallback then failed for a second reason: `entry-view-projects` does ship
(EntryShell.tsx:1579) and `/projects` does route to it, but it was asserted on
the 10s default budget right after `waitUntil: 'domcontentloaded'`. `apps/web`
mounts `src/App` via `dynamic(..., { ssr: false })`, so domcontentloaded fires
while only the boot shell exists — every suite that does this correctly waits out
`Loading Open Design…` with `T.long` first. Now routed via
`history.pushState` + `popstate` (the client router listens) instead of a full
reload, boot shell waited out, and both the view and the create button given
`T.long`.

`ensureRailOpen` was also called unconditionally, but it ends in a hard
`expect(workspace-home-rail-toggle).toBeVisible()` and that testid only renders
for `isPinned && active` (WorkspaceTabsBar.tsx:1474-1510) — from inside a project
it does not exist, so the helper hard-failed on a control this flow never needed.
Now best-effort and gated on `.entry`, kept only so the
`visual-new-project-modal` baseline does not churn.

`real-daemon-run.test.ts:547` hung on `/back to projects/i`, which matches
nothing on a project surface: `AppChromeHeader` owns that aria-label but is no
longer mounted anywhere, and ChatPane's top-left slot resolves
`onCollapse ?? onBack` while ProjectView passes both, so it is always
`chat-collapse-toggle` (deliberate, `884ed1085`). Replaced with a local
`leaveProjectForEntry` that clicks the pinned entry tab — the shipping way out —
so the isolation journey still leaves through real chrome instead of a URL jump.

Dropped `getActionablePoint` with its only consumer. Removing the reload from
~25 call sites across 15 suites was audited against the stub-order-sensitive ones
(settings-media-providers, new-project-ds-picker-*, api-empty-response,
visual-entry); all install routes before their first navigation.

Correcting my own earlier misattribution: `.ws-tab` / `.ws-tab-label` are
FileWorkspace's in-project file strip and are fully intact, `role="tab"` and
file-name labels included. #6156 rewrote WorkspaceTabsBar, which renders
`.workspace-tab` / `.workspace-tab__label` — a different component. Upload→tab is
also intact (`FileWorkspace.uploadFiles` still calls `openFile`). Five of those
six failures merely routed through the broken helper above; the sixth was the
dead back-button. No `.ws-tab` assertion was rewritten.

* fix manual edit across workspace pages (#6160)

* fix(collab): keep process-local presence alive when the collab transport is off (#6170)

`createVelaCliCollabClientFromEnv` returns `null` unless the run opted into
the vela-cli collab transport, but `server.ts` wired the presence routes with
an object literal of arrow functions closing over that client. A literal is
unconditionally truthy, so `registerCollabPresenceRoutes` always saw a `cloud`
dependency: its `deps.cloud ?? null` could never resolve to `null`, the
process-local `presence.present()` fallback became dead code, and every
presence request that reached the cloud branch dereferenced `null` and was
answered `502 collab_presence_unavailable`.

`POST /api/projects/:id/presence/leave` carries no shared-project
precondition, so it failed on any project id in any run without the
transport — every stable/prod packaged build (the packaged workspace-team env
is gated to the `feature-test`/`test` AMR profiles) and every plain
`tools-dev` run. `server.ts` is `@ts-nocheck`, so the null dereference was
invisible to `pnpm typecheck`.

Route the dependency through `createCollabPresenceCloudClient`, which lives in
the checked route module and states the invariant: a `cloud` dependency exists
if and only if a transport exists. Removing its guard is now a typecheck
error, so the bug class cannot come back at the construction site even while
`server.ts` stays unchecked.

* fix(web): scope desktop vibrancy to macOS (#6171)

* fix(web): remove the theme setting and force every install back to light (#6168)

Product removed theme selection: the workspace surfaces shipped for team
workspaces have no dark tokens, so dark mode renders a broken app.

Deleting the picker is not sufficient on its own. Every install that ever
opened it still has `theme: 'dark'` — or `'system'`, which resolves dark on a
dark OS — persisted in localStorage, and a stored value does not move when the
default does. So the theme is now coerced on READ, at all three points a
persisted value can reach the document:

- `loadConfig()` funnels `parsed.theme` through `resolveAppTheme()` and marks
  the config migrated so the coerced value is written back once.
- `applyAppearanceToDocument()` stamps `data-theme="light"` unconditionally.
  The attribute must be PRESENT, not merely non-dark: every dark CSS rule is
  gated on `html:not([data-theme])` / `html:not([data-theme="light"])`, and
  every JS theme reader (shiki, ConnectorLogo, SketchEditor, TerminalViewer,
  connectorBrandColor, MentionNode) falls back to `prefers-color-scheme` only
  when the attribute is absent.
- The pre-hydration inline script in `app/layout.tsx` stamps light before React
  mounts, outside its try/catch so a throwing storage read still leaves the
  attribute set.

Electron's `themeSource` defaults to `system`, which colours everything the web
layer does not own (macOS vibrancy glass, native menus/dialogs) on a dark-mode
Mac — including the splash, before the renderer's appearance IPC lands. It is
now pinned to light before the first window exists.

Removed surfaces:

- `AppearanceSection` in SettingsDialog, orphaned by #6156 (zero call sites),
  together with its `settings-general-block--appearance` styles. Its docblock
  claimed the control was "deliberately kept … NON-ALIGNMENT #9"; that decision
  is superseded, so the comment goes with the code.
- The onboarding welcome page's sun/moon toggle — the last reachable theme
  writer — and the `onThemeChange` prop chain behind it
  (App → EntryView/ProjectView → EntryShell → OnboardingView).
- The orphaned theme row in EntrySettingsMenu and its styles.
- i18n keys `settings.appearance`, `settings.appearanceHint`,
  `settings.themeSystem`, `settings.themeLight`, `settings.themeDark` across
  `types.ts` and all 19 locales.
- Analytics `trackSettingsAppearanceClick`, `SettingsAppearanceClickProps`, and
  the `settings_popover` `appearance` element.

`'appearance'` survives only as a legacy settings deep-link token that
`normalizeSettingsSection` folds into General, so an old link is not a type
error.

Red-first: tests/state/force-light-theme.test.ts and
tests/components/theme-settings-removed.test.tsx were written against the
unmodified branch and went red (11 failing) before any source change.

* fix(web): make the update surfaces read real data (#6163)

* fix(web): make the update surfaces read real data

#6156 (4e3161751) shipped two update surfaces whose every field was
invented. `EntryShell` held a private `updateReminderStage` machine keyed on
a literal `'1.4.6'` (the app ships 0.16.1), the rocket's confirm ran a 200ms
`setInterval` easing a fake bar toward 100% and then wrote "updated" to
localStorage, and the cover dialog listed three hardcoded English notes past
i18n over a committed 432 KB JPEG. Nothing in that path made a single
network, IPC or daemon call — a user who clicked "立即更新" watched a
progress ring finish and got no update.

Both surfaces now sit on the real machinery that already existed:

The cover dialog becomes the post-update highlights surface and REPLACES the
bottom-right `WhatsNewPopup` card rather than coexisting with it. Cover art,
release headline, bullets and link all come from the hosted highlights
document via `/api/whats-new`; the title states the running version from
`useAppVersion()` (`/api/version`). It keeps that card's show gate verbatim —
once per highlight `id`, only with content, only on the home surface after
the app returns on a new version — and its existing analytics
(`whats_new_popup` surface-view / click). Its footer is close + open-release;
there is no "cancel / update now", because reporting what already shipped is
all this surface does. Bullets come from a new `whatsNewNotesFromBody`, which
splits the document's `body` per line and strips operator list markers.

The rocket keeps its look and becomes the real updater's ready indicator in
the bottom-left rail footer, replacing that indicator's arrow glyph. It is
gated on the updater's own `shouldShowControl` (installer downloaded and
never opened, desktop only) and still drives `openUpdaterInstaller` →
`quitAfterUpdaterInstallerOpen` through the untouched panel, watchdog and
restart-safety preflight. The progress ring and percent face are gone on
purpose: the indicator only exists once the download has finished, so any
percentage it drew would be invented.

Deleted: `lib/update-reminder.ts`, `UpdateReminderDialog.tsx` and its module
CSS, `public/update-reminder-cover.jpg`, the now-dead
`entry-updater-menu__button/__glyph/__progress` rules, and the unused
`accountNotice` rail slot. The three `updateReminder.*` i18n keys and the
orphaned `whatsNew.dismissAria` are replaced by one `whatsNew.updatedTitle`
across all 19 locales.

A source-level guard (`tests/update-surface-real-data.test.ts`) pins the
invariant that outlives the deletion: no quoted `x.y.z` and no `setInterval`
in the update surfaces, and the placeholder module and cover art stay gone.

* fix(web): name the release-notes destination on the what's-new CTA

Product review of the five open calls closed all five; only the CTA copy
changes. The button opens the release notes, so it now says so —
「查看更新说明」 in Chinese (product's wording, verbatim), with the other 18
locales aligned on "view the release notes" instead of inheriting the old
"see what's new" tease. `whatsNew.cta` had exactly one consumer left (the
dialog) once it replaced the bottom-right card, so the key is repurposed in
place rather than forked.

The other four calls were approved as implemented, and two of them overturn
conventions the old comments still asserted, so those comments are synced:

  - `lib/whats-new.ts` documented the toast's deliberate Escape exemption
    (Escape hid the card WITHOUT spending the highlight). As a focus-trapping
    modal that exemption is retired — every close path marks seen — and the
    docblock now says which paths and why.
  - `contracts/api/whats-new.ts` described `title` as generic copy, `imageUrl`
    as an image "beside the copy", `body` as prose, and `linkUrl` as the "See
    what's new" link. The dialog reads them as headline / top-inset cover /
    one-bullet-per-line / release-notes link, so each field now documents the
    role it actually plays. Comment-only; no shape change.

* fix(web): never state the placeholder version on the what's-new dialog

Review catch (nettee, #6163): `useAppVersion()` reads /api/version at runtime,
so it necessarily boots on `APP_VERSION_PLACEHOLDER` and resolves a round-trip
later. This dialog rendered as soon as /api/whats-new resolved and printed the
hook unconditionally — so a highlights fetch that won that race painted
"Open Design 0.0.0 is here" on first frame. An invented version string, on the
one surface this PR exists to make truthful. The new suite could not see it
because it mocked the hook to an already-resolved value.

Fixed by falling back rather than gating the whole render: gating would delay
the dialog behind a second round-trip on slow networks, while the highlights
document already carries the running version in its `version` field — the
daemon stamps it for display (see contracts/api/whats-new.ts), which is what
fed the old bottom-right card's eyebrow. So `statedAppVersion()` prefers the
resolved hook, falls back to the document, and returns null when neither can
name one; on null the dialog waits, because highlights are worth nothing under
a headline that lies. The same derived value feeds the title AND every
`app_version` prop, so analytics cannot ship the placeholder either.

`APP_VERSION_PLACEHOLDER` moves out of ./provider into a new leaf
analytics/app-version.ts alongside an `isResolvedAppVersion()` predicate, so a
surface that must not PRINT a placeholder can test for it without importing the
analytics client. The provider keeps its own use of the constant.

Regression tests, red before this commit (`.red-evidence-version-race.txt`
reproduces nettee's exact string, `expected 'Open Design 0.0.0 is here…' not to
contain '0.0.0'`): the placeholder never paints while /api/version is in
flight, it never reaches the surface-view analytics, the title switches to the
running version once the hook resolves, and a document with no usable version
waits instead of inventing one. The suite's provider mock is now a mutable
holder so both sides of the race are reachable.

Also caught by this PR's own source guard: quoting the literal in a docblock
tripped it, so the comment names the constant instead.

* test(e2e): unmask the AMR runtime lane and repair the visual settings/workspace captures (#6174)

Three fixture defects, each verified by running the lane rather than reading it.

- amr-run-failure-recovery.test.ts was still `mode: 'serial'`. A second
  `describe.configure({ timeout })` cannot clear it — configure only
  overwrites the keys it is given — so one failure skipped the eight cases
  behind it and the lane reported them as "did not run". e2e/AGENTS.md
  forbids serial groups outright.
- configureVisualPage's `**/api/**` catch-all (added by #6126, which is on
  main) answered `/conversations` with 404. ProjectView mounts ChatPane only
  once a conversation resolves, and both listConversations and
  createConversation swallow a non-ok response, so the project opened with no
  conversation and no error, ChatPane never mounted, and every capture that
  enters the workspace died on `chat-composer` not existing. Supplies the
  conversation boundary the catch-all closed without replacing.
- The BYOK tab is named "API providers" since #5971 renamed
  `settings.modeApiMeta`; `getByRole('tab', { name: 'BYOK' })` matched
  nothing and three captures hung until the test timed out. The topbar BYOK
  capture also left `[MOCK_AGENT]` installed, contradicting its own "a BYOK
  config has no local agent" premise: an installed agent wins the popover and
  renders its model radiogroup instead of the BYOK rows.

Co-authored-by: lefarcen <ontf116@gmail.com>

* Home shell productization batch: message center, What's-new on release data, update-reminder & updater popup restyle (#6162)

* UI polish batch: import-dialog cleanup, settings toast placement, tab chrome fixes

- Plugin/skill import dialog: drop per-card icons, left-align card content
- Settings page: center the autosave pill under the top nav
- Avatar model menu: soften the pinned footer divider
- Workspace tabs: pinned entry tab always reads as Home; remove tab-search
  button and its popover
- Model picker: wider row gap in the two-pane browse popover
- Community header: unpin (scrolls with content)
- New update-reminder dialog assets and strings
- Keep entry-settings-button as the signed-out settings entry (e2e contract);
  align NextStepActions with the navPlugins key rename

* Update reminder: simulate in-place download progress instead of linking out

The strip's confirm now runs a staged progress state ('updating') with an
eased fake download bar, then marks the version updated — replacing the
jump to the GitHub releases page.

* test(web): retire AvatarMenu account-row suites after the popover simplification

The composer popover was reduced to a model picker (2026-07-24): the Open
Design account row — plan badge, balance, wallet fallback, upgrade/console
links — no longer renders there, so the nine suites asserting that surface
(including the workspace-balance and billing-permission gates that landed
upstream meanwhile) can no longer pass. Drop them and recast the remaining
account-row test as the guard for the new invariant: a signed-in AMR status
must render no account UI in the popover.

Also drop the stale onClose prop from the new upstream sign-out-confirm
test; the rail no longer takes one.

* Design-systems toast: anchor to the pane's top edge, pill radius

* Update reminder strip: progress ring on the button, percent face

While updating, the collapsed strip no longer stacks a label + bar above
the rocket button. Progress draws as a brand-green ring hugging the round
button's edge (5px band reserved so the button never shifts), and the
button face swaps the rocket for the bare percent — 'N%' while
downloading, 完成 (updater.done) at 100. The full 正在下载更新 N% label
stays reachable via the hover bubble and the ring's aria-label. Demo
payload version bumps to 1.4.6 so the once-per-version card re-arms.

* Home shell productization batch: message center in the account menu, What's-new on real release data

- Message center: entry moves into the account menu under 设置 (bell row);
  MessageCenter becomes controllable (hideTrigger + open/onOpenChange +
  onUnreadCountChange) and mounts persistently in EntryNavRail so unread
  polling survives the hover menu; unread shows as a red dot on the account
  avatar (and on the menu row); avatars go from squircle clip to 8px rounded
  squares; panel filter segmented control and mark-all-read go pill-radius.
- What's-new: the post-update highlights card now wears the shared
  release-dialog shell (cover / title / dotted notes / pill footer) while
  keeping its REAL /api/whats-new data source, show-once-per-id timing,
  soft-hide Escape/backdrop semantics, and analytics. The demo-payload
  update-reminder dialog and its simulated download no longer trigger; the
  stage machine and progress strip stay for the real updater-feed wiring.
- Rail chrome: brand logo and the in-rail collapse control are gone — the
  rail starts at the search box and folding lives in the pinned Home tab's
  toggle; the signed-out cloud tip bottom inset now matches its sides.
- Brand picker (modal variant): side-nav split — search + vertical category
  nav left, a roomy 2-up borderless gallery right, content flush to the
  column line with hover fill/ring bleeding past it; logo tiles lose their
  boxed edge; selected template chip and the composer mode pill share the
  same selected ring, with the mode glyph optically matched at 13px.
- Tab launcher: rows are icon + label only (descriptions dropped) with a
  rotating four-hue icon palette; the 新建空白页面 entry is paused behind
  ENABLE_BLANK_PAGE_WORKSPACE_ENTRYPOINT and its PageCreator suites skipIf
  on the same switch.
- Move-to-team confirmations: extracted MoveToTeamConfirmDialog, no backdrop
  blur (dim only), and the confirm keeps its brand-green label through the
  mention-home ink-pill restyle in every interaction state.
- Publish CTA: hover inverts to ink fill with brand-green icon + label.
- Plus assorted in-progress workspace polish (composer, viewer, settings,
  model icons) riding the same branch iteration.

* Restyle the updater ready popup to the update-reminder dialog language

Cover art on top (compressed 1296x555 jpg, natural aspect), the orange
accent icon tile removed, secondary action as a soft white pill and the
install CTA as a black pill with brand-green ink, and a stacked footer so
the silent-update checkbox keeps a full-width single line instead of
wrapping beside the widened pills.

* Signed-out rail: message-center entry under 设置

The signed-out rail has no account menu (where the 消息中心 row lives when
signed in), which left the message panel with no opener at all. The rail
item carries the unread dot.

* Composer row: pin chip glyphs against flex shrink

The row-wide min-width: 0 that lets chip labels ellipsize also let SVG
glyphs absorb the squeeze — a hard-narrowed pane rendered the design-system
palette icon tiny. Chips still shrink; their icons keep natural size.

* Bind Community Remix projects to the current workspace

Addresses mrcfps' review thread on apps/web/src/App.tsx:3303.

Product reproduction: 「通过 remix 后的方案, 不出现在我当前 workspace 里的
草稿了」 — stable repro in a team workspace.

`duplicatePluginAsProject` only sends the workspace/member identity headers
when its third `workspaceContext` argument is supplied, and the daemon's
`authorizeCreatedProjectWorkspace` deliberately reads a COMPLETELY headerless
create as a legal legacy/anonymous caller — `bindCreatedProjectToWorkspace` is
a no-op for a null context. Both remix call sites this PR adds (App.tsx's
standalone /community branch and EntryShell's community tab) omitted the
argument, so a remix performed inside a team workspace produced a project
bound to no workspace at all and therefore invisible to every workspace view.

Same enforceWorkspaceProjectMutation / created-project bypass class as
c0bce3b8f, fixed the same way: thread the already-resolved workspace context
that both files already hold, through the repo's existing
`resolvedWorkspaceContextForWrite` seam so an unresolved or unavailable
authority fails closed instead of silently creating an unbound orphan.

The second half of the review note — the follow-up `patchProject` whose `null`
was ignored — was a consequence of the same root cause: with the project
unbound, `enforceWorkspaceResourceMutation` finds no workspace row for the
caller's workspace and 403s the pendingPrompt seed, so the template prompt was
dropped too. Both requests now share one resolved authority, so they can never
disagree about which workspace they are acting in, and a still-null result is
reported instead of swallowed (without creating a second project, which is what
routing it through the existing catch would do).

Red spec first: apps/web/tests/components/community-remix-workspace-binding.test.tsx
drives both call sites in a team workspace against a fetch stub that reproduces
the daemon's create-then-bind contract. Red before this change (the create
carried no workspace headers, the project stayed unbound, and the seed PATCH
403'd), green after.

* Cover the refused-prompt-seed branch of the Community Remix fix

Second half of mrcfps' review ask on the same thread: prove what the flow does
when the follow-up `patchProject` still returns null after the binding fix.

The remaining null shapes are genuine transient refusals (daemon down,
membership revoked mid-flight, workspace locked between the two requests). By
then the copied project is real and bound, so the flow deliberately does NOT
fall into its own catch — that path creates a second, prompt-only project and
would strand the copy. It keeps the user on the remix and reports the dropped
seed. This case pins that: user lands on the remixed project, no second project
is created, and the failure is surfaced rather than swallowed.

* Re-anchor three style specs on the dogfooded UI

`Web workspace tests` went red on this branch because three CSS specs still
described the pre-dogfood layout the restyle replaced. Each one is re-pointed at
what shipped, keeping the invariant it was written to protect:

- `acceptance-visual-fixes` (recvpYDfW12NBu): the preset-thumbnail fix is the
  resting `scale(1.15)` framing transform, which is intact. Its hover partner
  went away with the family-wide cover-zoom removal (2026-07-27), so the spec
  now asserts the framing zoom on both media surfaces and separately locks the
  hover rules out — re-adding one would resample the covers soft again.
- `mention-popover`: the filter strip scrolls on one line instead of wrapping to
  a second row. The "no clipped labels" half of the contract is unchanged and
  still asserted (pills hold natural width, labels stay `nowrap`).
- `settings-polish`: the updater popup footer stacks the silent-update checkbox
  above a 50/50 action row. Long en labels must still wrap inside the checkbox
  column rather than overflow the panel, which the spec keeps checking.

* Return message-center focus to the rail opener that owns it

`hideTrigger` handed the message-center entry point to the rail but left the
component's internal `triggerRef` unattached, so `closePanel()` restored focus
to nothing. Opening focuses the portaled dialog, so every close — button,
backdrop, Escape, notification-settings — unmounted the focused node and dropped
keyboard focus to the document. Both external openers also failed to advertise
the dialog they own.

`MessageCenter` now takes a `returnFocusRef`: the host that hides the built-in
bell owns the duty that button used to serve. `EntryNavRail` points it at a
control that is still mounted after the close — the account trigger on the
signed-in branch, because the hover menu unmounts the 消息中心 row before the
panel opens, and the rail item itself when signed out. Both openers carry
`aria-haspopup="dialog"` and the live `aria-expanded`.

Red first: `EntryNavRail.message-center-entry.test.tsx` opens from each entry,
closes with Escape and with the close button, and asserts focus lands back on
the host control. All four cases failed before this change.

* Carry the menu popup contract onto the composer quick pills

The 插件 / 设计百宝箱 pills open the standalone `role="menu"` popovers in
ChatComposer, but they arrived without the contract of the ComposerPlusMenu rows
they replaced: no `aria-haspopup` / `aria-expanded`, no Escape handler, and no
focus return. Keyboard and screen-reader users could not tell a popup was open,
and Escape pressed while focus sat in the plugin search did nothing.

The pills live in ChatPane while the popovers live in ChatComposer, so the state
is plumbed both ways rather than duplicated:

- `onStandalonePanelChange` reports which popover is open, and each pill carries
  `aria-haspopup="menu"` plus its own `aria-expanded` — the same pair
  `ComposerPlusMenu` already puts on its trigger. No `aria-controls`: that
  surface does not use one, and this is not the place to invent a second
  convention.
- `openDesignToolbox` / `openPluginsPanel` take the opening pill as the
  return-focus target. Both popovers move focus inside themselves, so a
  dismissal has to hand it back, and the pill is the control the user came from.
- Escape closes through a document-level handler, mirroring ComposerPlusMenu's.

`dismissStandalonePanels` is deliberately only wired to the dismissal paths
(Escape, backdrop). Picking a plugin or an action keeps the plain setters,
because those hand focus to the composer input and pulling it back to the pill
would fight that.

No interaction changes: hover-open, the shared close timer, pill-to-popup
pointer travel and every selection path behave exactly as before.

Red first: `tests/components/ChatPane.quick-pill-popup-contract.test.tsx` runs
both pills through advertise / expand / Escape-from-inside / backdrop-dismiss and
asserts focus lands back on the pill. All six cases failed before this change.

---------

Co-authored-by: lefarcen <935902669@qq.com>

* Converge the visual Design Files prelude instead of clicking once (#6181)

`Playwright visual (settings-workspace)` went red on `[P2] captures the settings
BYOK surface` with a 10s `aria-selected` timeout: the Design Files tab sat at
`false` for all 23 polls with nothing re-clicking it. Seven other captures run
this identical prelude and passed, including the two neighbouring BYOK cases.

The defect is in the oracle, not the product. `prepareVisualWorkspaceFileList`
decided **once** whether to click — one instantaneous `isVisible()` probe of a
file row — and then asserted a state nothing retries. Any interleaving where the
workspace's own tab reconciliation lands around that single click leaves the
assertion permanently unsatisfiable for that attempt, and the lane runs
`OD_PLAYWRIGHT_FULLY_PARALLEL=1` with `retries: 0`, so it reports as a hard
failure rather than a flake.

`activateVisualDesignFilesTab` now converges on the goal state via `toPass`,
driving off `aria-selected` directly rather than through the file-row proxy —
the guard and the assertion were about different things even though
FileWorkspace derives both from one `activeTab === DESIGN_FILES_TAB` expression.
Repeat clicks are safe: the tab's handler is `setPersistedActive(DESIGN_FILES_TAB)`,
which is idempotent.

Why this is not a product regression from #6162: nothing in it touches tab
selection, tab persistence, or row visibility. Its only `design-files.css` change
deletes the hover cover-zoom, which cannot affect visibility, and a diff of
`apps/web/src` for `localStorage|activeTab|openTabs|restoreTab` comes back empty.
The lane's one green run before this failure (`4a6653186`, the #6174 repair of
the unrelated #5971 BYOK rename) is the whole basis for calling the baseline
clean.

Scope is the visual lane only — every caller is in `visual.ts` or
`visual-*.test.ts`, so the UI P0 lanes are untouched.

* fix(web): explain the AMR workspace-scope block instead of a dead send button (#6178)

An Open Design Cloud project run requires a resolved personal/team workspace
authority (21f452ffe). Failing closed there is correct and stays. Failing
closed SILENTLY was the bug: the send button went grey with no reason on
screen and no way out, while Home's equivalent dead end
(checkAmrBalanceGate -> AmrBalanceDialog reason `signed_out`) hands the user
an in-app sign-in. e2e/ui/amr-logout-requires-relogin.test.ts exists to
protect exactly that route, which a disabled composer can never reach.

Adds a classifier (`amrWorkspaceScopeBlock`) that turns "gate closed" into
the remedy that clears it, and a composer-adjacent notice that renders it:

  signed_out — the same AmrLoginPill action and `chat.amrBalanceGate.signInCta`
    copy Home's balance gate uses, so one identical action clears it from
    either surface.
  unresolved — a re-read of the project's workspace authority, exposed as
    `ProjectWorkspaceScopeState.revalidate`. An account action would be a guess.

The gate itself is untouched: `projectRunWorkspaceScopeReady` never consults
the classifier, and the `disabled: true` invariant stays asserted.

* test(updater): join the installer-reinstall floor across release and client (#6167)

* test(updater): join the installer-reinstall floor across release and client

A payload update never replaces the Electron outer shell, so a release whose
shell changed can only reach an old install through the installer. The
`control.launcher.version.{min,url}` floor is the one mechanism that forces
that, and it spans three owners: tools/release resolves the channel policy,
the feed carries it, and the desktop updater enforces it against the
physically installed outer version. Every side had unit coverage; nothing
joined them, so the only place a break in the chain would surface was a
release publish.

Adds six specs to the already-allowlisted packaged-launcher cross-boundary
suite, driving the real release-side resolver into a real feed and then into a
real packaged update check:

- an outer below the floor takes the installer, not the payload, and the
  launcher pointer proves nothing was adopted;
- the floor is judged by the installed outer rather than the payload it is
  running (two installs on the same payload, only the stale shell diverted);
- an unreadable installed outer fails closed as `outer-version-unreadable`
  with no installedVersion to render;
- a launcher schema beyond this client diverts on the ABI axis alone;
- with no floor configured a below-floor outer still swallows the payload,
  which is why a shell-changing release must set the repo vars deliberately;
- a floor above the release version stays refused.

Falsifying `remoteRequiresReinstall` to compare against `config.currentVersion`
-- the semantics the released 0.15.1/0.16.1 shells actually ship -- turns two
of these red, including the unreadable case being mislabelled as below-min.

Also pins the floor's operational shape against real channel version formats:
a bare stable floor sorts above every same-base `-beta.N`/`-preview.N`/
`-prerelease.N`, so inheriting it into those lanes hard-fails their publish.
The specs name that trap and pin the one-explicit-pair-per-lane configuration
that avoids it.

* test(updater): drive the reinstall floor on Windows too, off the real outer

Review (nettee) caught these specs running macOS only: the feed described
`platforms.mac`, the payload writer built a `.app`, and the updater config was
pinned to arm64/darwin. So `resolveInstalledOuterVersion`'s Windows branch —
`dirname(launcherLaunchPath)/resources/open-design-config.json`, a different
lookup from the mac bundle path — and the `installer` artifact shape never ran.
Since the installs most exposed to a shell/payload mismatch are the oldest ones,
leaving Windows uncovered left the half most likely to break unguarded.

The specs now run through `describe.each` over a per-platform target table
carrying the launch-path shape, outer-config location, installer artifact key,
and payload extractor.

They also stop using `OD_UPDATE_INSTALLED_VERSION`. That override returns before
the platform branch it was supposed to exercise, so it was masking the code under
test. Each scenario now materializes a real installed outer package at the real
per-platform location and lets the updater read the version off disk — which
also makes the unreadable-outer case genuinely unreadable rather than simulated.

Falsifying the Windows lookup (collapsing it into the macOS path) fails exactly
the two Windows specs that depend on reading a real installed version, while all
twelve macOS and publication-policy specs stay green.

* fix(web): partition the workspace plan nameplate like the wallet (#6182)

A workspace Vela Web shows as 免费 rendered as 专业版 Plus in the client's
account menu, while the 额度 row in the same card correctly read $0.00.

Money and plan travelled different paths. `GET /api/workspace/billing`
returns three independently scoped things, and only two of them are about
the workspace: `workspaceBalance` / `workspaceSnapshot` are proven for the
exact workspace + member, while `summary` is the caller's VELA ACCOUNT
billing (`workspaceId: null` by contract; the daemon reads it with one
unscoped `fetchBilling()` regardless of `?workspaceId=`). The rail's plan
nameplate read `response.summary` raw, so an account holding a personal
Plus reported `plus` in every workspace — a value that cannot change when
the workspace changes, because it was never about the workspace.

`workspaceBillingSummaryForContext` makes the partition key explicit, the
same `workspaceId` + `workspaceMemberId` pair money already uses:

  - personal workspace — the account IS the scope, summary passes through;
  - team workspace — plan comes from the context-authorized snapshot, and
    the account tier may stand in only when it is itself team-namespaced,
    since a personal tier cannot name a team workspace's plan.

That last clause keeps the 飞书 P0 fix intact: B omits planId/billingState
for a non-owner and `workspaceSnapshot` is an additive capability, so a
team-namespaced account tier is the only surviving evidence that a paying
MEMBER's team is subscribed.

`useWorkspaceBilling` is now this projection over the ambient context, and
EntryShell / SettingsDialog consume it instead of the raw summary. No
interaction, layout, or copy changes.

* Revert "fix(web): explain the AMR workspace-scope block instead of a dead send button (#6178)" (#6184)

This reverts commit ea259eb402.

* fix(daemon): resolve an unbound project against the caller's current workspace (#6185)

* fix(daemon): resolve an unbound project against the caller's current workspace

Every project must resolve to a workspace; when the project itself has no
binding, the workspace is the one the caller is currently acting in. There is no
other candidate.

`GET /api/projects/:id/workspace-scope` read only the persisted
`workspace_projects` row and the membership directory — never the request's
`x-od-workspace-*` identity — so a project with no row answered `unbound` for
every caller forever. `unbound` makes `projectWorkspaceScopeAuthorizesAmr` false,
which disabled the chat composer's send button for an Open Design Cloud run on
that project permanently, with nothing the user could do to clear it. #6178 wrote
user-facing copy for that state instead of fixing it (reverted in #6184).

`resolveProjectWorkspaceScopeForCaller` wraps the existing resolver instead of
branching inside it, so `resolveProjectWorkspaceScope` stays byte-identical and
the bound path is provably untouched. The fallback re-enters that resolver with a
synthetic binding naming the caller's own workspace, so the resulting
`workspaceMemberId` comes from the membership directory and never from the
request header: a caller can only select among workspaces their signed-in
identity is genuinely an active member of, and anything the directory cannot
confirm degrades back to `unbound` rather than to `unavailable` on a workspace the
project was never bound to.

Two cases keep today's behavior, both pinned by spec:

  unavailable — all three of its return sites. A project pinned to workspace X
    read by a member of Y answers X. The scope carries `workspaceMemberId`, the
    wallet that pays for the project's runs; resolving to Y would bill Y for X's
    project. Reachable because `GET /api/projects/:id` has no workspace gate, so
    a deep link or a workspace switch lands there.
  no caller identity — signed out, or a plain curl, has no current workspace to
    fall back to, and inventing one is worse than answering "none".

The fallback deliberately does not persist a binding.
`reconcileUnboundProjectBeforeMutation` was the preferred shape going in and is
wrong here: its own docblock is explicit that a passive read must not hand out
ownership just because it ran first, and this endpoint is a GET. Writing a row
from it would let whichever workspace opened the project first claim authorship
on no user intent, and would race two clients in different workspaces.

* fix(web): stop pre-emptively blocking an AMR send that has a wallet

An Open Design Cloud run is billed to the CALLER's own wallet, so the only
defensible client-side veto is "there is no billing principal at all". The gate
instead required THIS PROJECT's workspace scope to resolve, and blocked the send
whenever it did not: an unbound project, a membership-directory read that
transiently failed (offline / 504 / timeout all collapse into one `ok: false`
upstream), a workspace that is billing_past_due or locked, or a team the caller
has since left. In every one of those the user is spending their own quota.

It is also not the enforcement point. Real enforcement is server-side — the
daemon's `WORKSPACE_CONTEXT_REQUIRED` 401 plus vela's own billing check. The
client gate could only convert a request the server would have answered into a
dead, unexplained button, which is strictly worse than an honest server error.
It arrived as 21f452ffe, whose entire message is "fail closed on unresolved
workspace authority" with no body and no stated product requirement.

`workspaceIdentityCanBillAmr` names the invariant on the identity read, and the
gate now admits either witness of a billing principal: that identity, or a
project scope that already resolves to an explicit personal/team principal.
Strictly a widening — every state it newly admits was previously blocked, and
nothing previously admitted becomes blocked.

Deliberately NOT treated as "no wallet":

  loading — the identity read holds no answer yet. Reporting "signed out" on a
    frame that has not heard back is the bug shape this replaces.
  failure 'unavailable' — a transient outage taught us nothing about the user.
  failure 'unsupported' — an old daemon with no workspace endpoint keeps its
    legal pre-workspace behavior.

A genuinely signed-out caller — a settled, authoritative read that came back with
no workspace — stays blocked: there is no wallet, so the run cannot be billed and
cannot succeed.

No UI, no notice, no copy: #6178 wrote copy for this dead end instead of removing
it, and #6184 reverts that. This removes the dead end.

* fix(web): point every console entry at /dashboard, drop the wallet target (#6165)

* fix(web): point every console entry at /dashboard, drop the wallet target

Product ruling: there is no wallet page in the console's information
architecture any more. Balance, manual top-up and the auto-recharge policy were
all rehomed onto the console dashboard (vela #1055), so every entry that used to
open `/wallet` now dropped users on a surface the product no longer navigates
to.

Retargets all of them:

- `AMR_CONSOLE_URL` / `DEFAULT_AMR_RECHARGE_URL` and the per-profile console
  map (prod / test / local) now resolve `/dashboard`, keeping the
  `?source=open_design` attribution param.
- `teamConsoleUrl`'s `billing` section — the team account menu's 「额度 $0.00」
  row — is a plain dashboard visit.
- `teamConsoleUrl`'s `plans` section, `amrPlansUrlForProfile` and
  `amrPlansUrlForWorkspace` — the free-tier 「升级」 button and every other
  Upgrade affordance — send `billing=plan` instead of the wallet page's
  `view=plans`.
- `amrWalletUrlForWorkspace` is renamed `amrConsoleUrlForWorkspace` so the name
  matches where it now points.

`billing=plan` is the console's one state-aware upgrade intent: its dashboard
resolves it against the workspace's real subscription state, so a personal owner
lands on the personal plan modal — the dialog the console's own 「升级订阅」 hero
button opens, which is what product asked the free-tier 「升级」 button to reach —
while a team owner still gets checkout or change-plan. The team `upgrade` section
keeps sending its more specific `billing=checkout` hint, but that is now only a
hint: powerformer/vela#1127 makes the console fall back to the dialog that
actually matches, so a stale `hasActivePlan` can no longer strand the user on a
bare Overview page (recvpSQKna0LwR).

Comments carrying earlier live-verification findings are updated wherever this
change invalidated them, rather than left contradicting the code.

No new user-facing copy, so no locale changes: the affected labels are all
action-named (「充值」/「升级套餐」), never destination-named.

* fix(daemon): retire the wallet target in the AMR recharge link too

The daemon builds its own recharge URL for AMR balance failures — it lands in
the failure message and in `actionUrl`, which the client renders as the chat
error card's clickable link — and it still pointed at the console's retired
wallet page. Same product ruling as the client-side retarget: balance and manual
top-up report on the console dashboard now (vela #1055), so this link has to go
there.

Every existing assertion in `vela-errors.test.ts` referenced the constant
symbolically, so all of them would have kept passing while the link sent users
to a surface the product no longer navigates to. Added one that pins the literal,
which is the red spec for this change.

Also retargets the two e2e assertions that pinned the old destination
(`e2e/tests/amr/insufficient-balance.test.ts` on the daemon's emitted URL,
`e2e/ui/visual-workspace.test.ts` on the avatar upgrade link's `view=plans`), and
the web-side fixtures that mimic the daemon's emitted string so they stop
documenting a URL it no longer produces.

* chore(release): bump feat/workspace-team to 0.16.2 (#6188)

The published beta feed is self-inconsistent: metadata.json carries
baseVersion 0.16.1 alongside betaVersion 0.16.2-beta.144, because
0.16.2-beta.144 was published with an explicit release_version override
while every manifest on this branch still said 0.16.1. Every subsequent
beta build now fails its own consistency check:

  [release-beta] beta metadata.json baseVersion 0.16.1
                 does not match betaVersion 0.16.2-beta.144

`force` does not help — it only waives "base must exceed latest stable",
not the baseVersion/betaVersion match.

Raising the branch's base version to 0.16.2 makes the next beta
(0.16.2-beta.145) consistent with its own metadata, and removes the need
to pass force at all. Same 18 manifests the release bot touched in
23f9fcaac; test fixtures that mention 0.16.1 as data are left alone, as
that commit also left them.

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(collab): keep a project's "last changed" time out of sync's hands (#6189)

A member who opened the client hours later and pulled a shared project saw
「刚刚更新」 on a project nobody had touched, and the home 最近项目 strip kept
rendering the previous workspace's cards for a beat after switching workspaces.

Two independent defects, both reported by the product owner on a packaged
client.

1. `projects.updated_at` (and the `workspace_projects.updated_at` the project
   list folds into it with `MAX`) is what the project card shows and what the
   list sorts by, so it must answer "when did a person last change this
   project's conversations, files, or name". Sync was answering it too:
   `materializePulledTeamMirror` carried the origin's timestamp into the
   project row but not into the binding it wrote in the same transaction, so
   `MAX` surfaced the pull's own clock on EVERY pull. Four more sync writers
   had the same shape. `SYNC_KEEPS_UPDATED_AT` names the invariant and the
   sync paths now either carry the origin's time or keep the row's existing
   answer.

2. A loaded project list describes exactly one workspace identity, so leaving
   that workspace invalidates it. The re-list on switch runs in an effect —
   after the commit — so the browser painted workspace A's cards under
   workspace B's identity first. The list is now dropped during the render
   that first observes the new scope, so no frame shows another workspace's
   projects. No extra request: the refetch already existed and was already
   keyed on the resolved workspace.

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(web): key every workspace read cache on who is asking (#6198)

`coalesced-get` and `shared-cancellable-get` are CACHES (1s share window), not
just in-flight dedupers, so a key that omits the caller's identity hands the
previous workspace's answer to the next one — and an in-flight entry is joined
with no window at all.

Three reads were keyed on a constant:

- `useTeamMembers` — `'workspace-members'`. `GET /api/workspace/members` answers
  for whichever workspace the DAEMON is active in and reads nothing off the
  request, so the roster is a per-workspace answer with no per-workspace URL.
  A leaked roster renders the previous workspace's teammate name on project
  cards (`RecentProjectsStrip.resolveCreator`). Now keyed on the workspace
  identity, which also makes the hook re-read on a switch instead of waiting out
  its 15-60s poll, and a late read for the identity the user left no longer
  overwrites the newer roster.

- `useWorkspaceContext` — `'workspace-context'`. This is the read that
  ESTABLISHES the identity, so it cannot be keyed on the identity it fetches and
  has no workspace argument to borrow (the switch is a separate
  `PUT /api/workspace/active`). It is keyed on the identity GENERATION instead:
  one token per deliberate identity change, so a single broadcast heard by a
  dozen mounted consumers still collapses to one request, while two switches
  inside `forceCoalescedGet`'s 250ms burst window stop being mistaken for one.

- `useProjectWorkspaceScope` — `project-workspace-scope:<id>`. The scope carries
  the `workspaceMemberId` that pays for the project's runs, so a shared key
  serves one member the wallet of another.

The same read also fetched HEADERLESS, which made #6185's daemon fallback
unreachable: `GET /api/projects/:id/workspace-scope` resolves an unbound project
against the caller's current workspace, but only when the request carries the
caller's `x-od-workspace-*` identity. It now sends `workspaceProjectHeaders`,
the same set every other workspace-aware project read uses — and waits for the
caller's own identity to settle first, so a project open still costs one request
instead of a wrong headerless one followed by a corrected one.

* fix(collab): carry B's workspace name for personal workspaces too (#6199)

B names EVERY workspace, personal included: vela's CurrentWorkspaceContext
requires `workspaceName`, derives "«owner»'s workspace" for an unnamed
personal one (vela #964), and an owner may rename it outright.

The daemon read that field and then threw it away unless the workspace was a
team, because the only name on WorkspaceCollabContext was `teamName`. The
switcher's collapsed label therefore had nothing real to show for a personal
workspace and fell back to a hardcoded English "Personal workspace" until the
user opened the dropdown and the separate workspace-directory read landed.

Adds `workspaceName` to the context contract and carries it for both workspace
types, in the vela mapping and in the directory-item synthesis. `teamName`
stays team-only: it is the team switcher's field and doubles as an "is a team"
signal, so nothing may start reading it for a personal workspace.

The web side consumes the context the shell already holds at startup, so the
label is correct on first paint and NO new request is added — the diff adds
zero fetch call sites. The dev provider parses `workspaceName` too so the
dev/demo lane can drive a personal workspace's label the way B does.

Note this is not what the reported screenshot shows: there, B's real name for
that workspace IS the literal "Personal workspace", because an in-place team
upgrade flips the default workspace's stored type to 'team' and vela's
owner-derived naming then no longer applies. That half is upstream.

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(web): stop offering 升级 at the top plan tier (team_max) (#6205)

A 团队版 Max workspace is the top of the ladder — nothing is above it — but
both upgrade entry points still showed 「升级」, and clicking it could only
reopen the plan the user already holds.

The two surfaces got there two different ways, and neither shared a rule:

- The account menu's billing card gated on `billingUpgradeUrl &&
  canManageBilling` — a destination check and a permission check, with no
  TIER check at all. Every tier saw the button.
- The Settings AMR card asked a personal-ladder question
  (`canUpgradeVelaPlan`) about `amrCardStatus.account.plan` — vela's
  ACCOUNT-scoped login projection, which reports `free` for a user whose
  entitlement is held by a team workspace. So a team_max owner measured as
  "free" and got the button, while the PlanBadge beside it correctly read
  Max off the workspace-resolved tier.

Both now ask one predicate, `canUpgradeFromPlanTier`, about the same
resolved tier each surface already LABELS, so the button can never
contradict the nameplate next to it.

Team and personal tiers are two ladders, so the predicate reads the
team-namespaced id whole instead of stripping the prefix and asking a
personal question. Per the product ruling — 「个人档位都是要显示可升级的,
最顶的就是团队 max」 — personal `max` keeps the affordance: that user can
still move onto a team plan, and vela #1146 deliberately routes their
click to the Team upgrade dialog. Only `team_max` loses it.

Destination logic (`workspaceUpgradeUrl` / `amrPlansUrl`) is untouched;
this only decides whether the affordance renders. No copy, no new i18n
keys.

* fix(web): keep Send alive after using a community template (#6206)

Clicking 「使用」 on a community template seeds the template's own brief into
the composer and binds the template as the run's driver. For a template whose
`od.inputs` are `required: true` with no `default` -- the real
`example-html-ppt-pitch-deck` ships three such fields -- the bind left
`active.inputsValid` false, and `submitDisabled` folded that into the send
button. The user saw a full prompt next to a permanently dead Send whose
tooltip still read "type something to run". Typing did not help: the gate was
never about the prompt.

Those fields have no input surface on that path. #3645 removed the Home
composer's inline plugin-inputs form, and the template's query carries no
`{{...}}` placeholders, so prompt -> inputs write-back cannot reach them
either. A gate on values the user cannot supply anywhere is a dead end, not a
guard.

Narrow it to where it is actionable: `requiredInputsAreUserFillable` keeps the
gate when the media composer renders the fields as controls, or when the
tracked query template actually names the missing field as a `{{placeholder}}`
that editing the prompt writes back. A tracked template alone is not enough --
re-applying a plugin without an explicit template derives one from the
manifest, placeholders or not, which is the shape a reload lands in.

The daemon stays the authority: `validateInputs` still throws
MissingInputError. Since the seeded path no longer pre-empts the send, its
rejection now reports the fields it rejected instead of a generic "Failed to
apply", reusing the existing pre-submit wording.

* fix(collab): give every created project a workspace home (#6201)

* fix(collab): give every created project a workspace home

A project created with no `x-od-workspace-*` headers got no
`workspace_projects` row and still returned 200, so nothing surfaced the
loss. `GET /api/projects/:id/workspace-scope` then answers `unbound`,
which strips the workspace off the run request (`ProjectView`'s
`projectRunWorkspaceContext` -> an Open Design Cloud run nothing can
bill) and blanks the balance/plan area while that project is open
(`AvatarMenu`). The same orphan is also denied its first run outright by
`enforceWorkspaceResourceMutation`.

Headerless creates are not anonymous callers asserting "no workspace" —
they are callers that could not say which workspace they meant:
`od project create` and the MCP `create_project` tool mint no headers at
all, the web duplicate / design-system-copy / plugin-share paths read a
ref that drops the context's `loading` flag, and Orbit + scheduled
routines have no request to read headers from. So a create that names no
workspace now binds to the one this daemon is signed in to, read through
`workspaceContext.lastKnown()` — zero-network, synchronous, no failure
mode. A daemon that has resolved no workspace still binds nothing, which
keeps `unbound` correct for the signed-out single-player user.

Six creation paths were writing no binding at all, not merely skipping it
when headerless: the plugin share-project task, Orbit run projects,
scheduled routine projects, the library capture-as-page exit, the
project-location scan importer, and the `ds-*` project backing a design
system's editing workspace. All six now bind.

Deliberately unchanged: the collab pull's register-on-pull and
shared-project placeholder. Those are a teammate's shared project, whose
correct binding is written by `materializePulledTeamMirror` with
`visibility: 'team'`; claiming them into the reader's ambient workspace
as personal would misattribute someone else's resource.

No creation path gains a new failure mode. `createdProjectWorkspaceHome`
never refuses — a partial or denied header identity degrades to the
ambient workspace rather than turning a working 200 into a 4xx.

On the web side `createPluginShareProject` was the one create in
`state/projects.ts` with no `workspaceContext` parameter at all, so it
sent no workspace identity ever. It now forwards the context when known,
best-effort — deliberately not `resolvedWorkspaceContextForWrite`, which
throws while the identity read is in flight and would add a new block to
a path that works today.

* fix(collab): verify an asserted workspace before persisting it

`createdProjectWorkspaceHome` resolved a created project's workspace with
`resolveCreatedProjectWorkspace`, which only PARSES headers. It trusted
the caller-provided member status and permissions and never checked that
the workspace/member pair exists in the membership directory or agrees
with the daemon's last-known state. `x-od-workspace-*` headers are an
unauthenticated hint, so any local caller — the `od` CLI, a plain curl, a
compromised page — could assert an arbitrary pair and get a project bound
to a workspace it has no membership in, with a fabricated
`createdByWorkspaceMemberId`.

"Do not fail the create" and "trust unverified input" are not the same
constraint. The helper now VERIFIES, then DEGRADES — never rejects, never
trusts:

  1. asserted identity that verifies      -> bind to it
  2. asserted identity that does NOT      -> bind to the daemon's ambient
     verify, including an unreadable         workspace instead, or leave
     authority ("cannot confirm")            the project unbound
  3. nothing asserted                     -> ambient, as before

Creation still never answers 4xx, so the six previously-never-bound paths
keep binding and a signed-out daemon still creates usable local projects.
An unverifiable claim is simply never written as fact.

Verification is not re-implemented. It is `authorizeCreatedProjectWorkspace`
— the same directory lookup `POST /api/projects` gates on, which also
returns the DIRECTORY's authoritative context rather than the caller's
claimed one — plus `withLastKnownMembership`, the same cross-check the
mutation gates apply (now exported rather than copied). Only the failure
behavior differs: they refuse, this degrades. An absent directory fetcher
remains `authorizeCreatedProjectWorkspace`'s own documented local/dev
compatibility path, not a second weaker definition of verified.

Route modules now take one `CreatedProjectWorkspaceResolver` dep instead
of re-threading the three authorities, so there is a single notion of
"verified" per daemon.

* test(daemon): drive brand routes through the created-project workspace resolver

The brand harness constructed `registerBrandRoutes` without the new
`resolveCreatedProjectHome` seam, so the binding became a no-op and
"binds a freshly extracted brand project into the caller's ACTIVE team
workspace" went red. Inject the same production resolver `server.ts`
builds, in its no-directory (local/dev) configuration, so the test keeps
asserting the product behavior through the real seam instead of a
header-trusting shortcut.

---------

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(web): move the update-ready rocket above the rail account row (#6208)

* fix(web): move the update-ready rocket onto the rail account row

#5517 removed the entry topbar and parked the updater host in the rail
footer, which put the green rocket on its own line UNDERNEATH the account
row — bottom-left, detached from the identity it belongs to. Product
reported it from a real client: the rocket belongs at the right edge of
the name + plan wordmark line.

Move the mount instead of nudging it with CSS: the rail now owns the
updater host through an `updaterSlot` prop and renders it inside the
account row (`.entry-nav-rail__account-row`), as a SIBLING of the account
trigger so it never steals the account-menu click target. The rail footer
stays as the fallback home for the signed-out shell, which has no account
row to ride, so the rocket's appearance conditions are unchanged.

Same change also un-clips the 「更新已就绪」 prompt. The host is pinned to
the bottom of the rail, so a `top: 0` panel grew downward off the bottom
of the window and the copy was cut off mid-sentence. Anchor its BOTTOM
edge to the host instead, the same way `.entry-help-popover` already does
for the same reason.

Side effect worth having: the rail no longer grows a 52px footer strip
the moment an update lands, so the account row stops jumping.

* fix(web): put the rocket ABOVE the account row, right-aligned

Placement correction. 「放到名字啥的上面的右侧」 was read as "on the name
row, right side"; product clarified 「应该是在名字那个横条的上方,靠右对齐」
— the rocket gets its own right-aligned strip ABOVE the identity row, not a
slot inside it.

The strip (`.entry-nav-rail__account-updater`) is mounted unconditionally so
it stays the account trigger's immediately-preceding sibling, and carries
`:empty { display: none }` so an idle rail reserves no height for it. That
rule is load-bearing: without it every user without a pending update would
get a blank 34px row plus the container's 6px gap pushing the account area
up — a worse regression than the placement it fixes. Measured in a real
browser at 1280x900: the account trigger sits at y 850-884 in BOTH states,
so nothing shifts.

Everything else from the first commit stands: the rail owning the host via
`updaterSlot` instead of an opaque `footerExtra`, the sibling-not-child
relationship to the trigger (now moot in the best way — a strip above the
row cannot be button-in-button), the `bottom: 0` panel anchoring that
un-clips 「更新已就绪」, and the footer fallback for the signed-out shell.

* fix(web): stop the home model list offering picks the chip cannot honor (#6209)

On the Home composer, clicking a model in the chip's compact list did
nothing — the chip kept showing the previous model.

The compact list rendered `currentAgent.models` raw, so a model above the
caller's plan (`enabled: false`, straight from `vela model list --json`)
appeared as an ordinary selectable row. Clicking it wrote the pick to
config, `normalizeAgentModelChoice` reported the plan fallback instead, and
the re-normalization effect in `InlineModelSwitcher` persisted that
fallback — so the chip snapped back with no explanation, and the user's
previous pick was overwritten by the default in the process.

The sibling surface (`AvatarMenu`, the project composer's model list) had
already hand-rolled `agent.id === 'amr' && model.enabled === false` for
this. Copying the rule per surface is what let the newest list drop it, so
this introduces one definition instead: `agentModelIsSelectable`, defined
to be at least as strict as `normalizeAgentModelChoice` (pinned by a test)
so a gated row can never be a pick the config would revert. Both surfaces
now ask it, and every model write inside `InlineModelSwitcher` — the
compact list and the execution-settings picker — goes through a single
`applyAgentModel` sink that asks the same question, leaving no second path
for a future list to bypass.

Locked rows now read as locked: dimmed, `aria-disabled`, a lock glyph, the
existing `settings.amrModelUpgradeHint` reason, sorted below the available
models, and a click routes to the plans page instead of silently doing
nothing.

* fix(collab): verify the asserted workspace before claiming an orphan project (#6213)

* fix(collab): verify the asserted workspace before claiming an orphan project

`reconcileUnboundProjectBeforeMutation` claims a project this daemon has
never bound to ANY workspace into the current mutating request's
workspace, so a true orphan is not denied its first mutation by
`enforceWorkspaceResourceMutation`'s `!row -> false` rule (recvqbhor3pai2).

It resolved that workspace from `workspaceProjectContextFromRequest(req)`
— header PARSING with no authority check — and stamped
`createdByWorkspaceMemberId: ctx.workspaceMemberId` from the same headers.
`x-od-workspace-*` is an unauthenticated hint any local caller can forge,
so a plain curl could claim someone else's orphaned project into a
workspace it has no membership in AND install itself as the author.
Authorship is the dangerous half: `workspaceResourceAccess` derives
`selfCreated` from it, and that bit is what grants a non-privileged member
mutation rights over the row.

Both the workspace and the authorship now come from
`resolveCreatedProjectHome` — the same verify-then-degrade resolver #6201
introduced for created projects:

  - asserted identity VERIFIES against the membership directory -> claim
    it, attributed to the DIRECTORY's member id, not the header's;
  - it does NOT verify (foreign, inactive, permissions disagree,
    last-known says removed, or the authority is unreadable so it cannot
    be confirmed) -> claim the daemon's own ambient workspace instead,
    attributed to that verified session's member id;
  - neither available -> write nothing, and let the pre-existing gate
    answer.

The `null`/`'missing'` early return is unchanged and load-bearing, which
is why this does not reuse `createdProjectWorkspaceHome`'s own third
branch: the mutation gate's HEADERLESS branch answers 401 as soon as any
row exists for the resource, so claiming on a request that asserts
nothing would convert a working headerless mutation into a 401.

A verified caller is unaffected — `workspace-projects.test.ts`'s
recvqbhor3pai2 regression case still passes unchanged. A caller that
cannot prove membership over a never-claimed project is now refused by
that pre-existing gate instead of being let through on a binding it had
just invented for itself; #6201's e2e case is updated to assert the
persisted binding rather than the mutation's status, since both outcomes
satisfy the property it pins.

* fix(collab): persist only an asserted-and-confirmed workspace on an orphan

Review follow-up (mrcfps). `resolveCreatedProjectHome` is not a pure
verifier: by contract a foreign, inactive, removed, or unconfirmable
assertion degrades to `workspaceContext.lastKnown()`. Reconciliation then
persisted that ambient context onto a PRE-EXISTING orphan before the
mutation gate had rejected the asserted caller — and the
`getWorkspaceProjectByProjectId` guard one line above makes that write
sticky, so the rightful verified workspace could never reconcile it
afterwards. A forged request, or a legitimate one during an authority
outage after the active workspace changed, would permanently mis-file the
project.

Creation and reconciliation are not the same risk, which is the
distinction the original patch missed. Putting a BRAND-NEW project in the
ambient workspace takes nothing from anyone — that is #6201 and it stays.
Putting an EXISTING orphan there may take it from the workspace it
actually belongs to.

So a degraded ambient authority is persisted only when it names the very
pair that was asserted. Verified assertions resolve to the asserted pair
by construction (the directory lookup is keyed on it), so they are
unaffected; outage continuity still works for the ordinary legitimate
caller, whose client took its headers from this same daemon and therefore
agrees with it; a mismatch writes nothing at all.

Not in tension with 「所有 project 都应该能找到 workspace」: the
display-side fallback (#6185) already renders an unbound project inside
the caller's current workspace, so nothing reads as "unknown". The
binding row is a separate, durable fact and stays conservative.

Adds the fixture-matrix case the review asked for — a populated but
DIFFERENT ambient workspace, asserting the orphan stays unbound. The mock
authority's `/current` is now flippable so the fixture can create a true
orphan while ambient is absent and then populate ambient deterministically
through `GET /api/workspace/context`, rather than depending on a poll tick.

---------

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(web): use neutral Windows app background (#6218)

* fix(collab): let a headerless caller mutate its own daemon's project (#6216)

* fix(collab): let a headerless caller mutate its own daemon's project

`enforceWorkspaceResourceMutation`'s headerless branch answered
401 WORKSPACE_CONTEXT_REQUIRED as soon as ANY `workspace_projects` row
existed for the resource. That was survivable while headerless creates
left projects unbound. #6201 made every created project get a workspace
home, so the two rules combined into a project its own creator cannot
touch:

    od project create     -> 200, and now writes a binding
    od project duplicate  -> 401

The `od` CLI never sends `x-od-workspace-*` — only `od workspace …`
builds those headers — so this is not an edge case for it. It is every
CLI mutation of every project the CLI created, which breaks the
embeddability contract `AGENTS.md` makes the CLI responsible for.
Reproduced end to end through the real binary against a daemon-only
process (`od daemon start --headless`, no web, no browser).

INVARIANT, now named in `headerlessMutationAllowed`: every mutation
resolves to a workspace identity. A request that ASSERTS one is judged on
that assertion; a request that asserts NOTHING is the local daemon's own
signed-in user and is judged as that identity. Being unable to name a
workspace is not the same as having no standing in one — the same
"nothing asserted -> ambient" fallback the create path already applies,
so the gate and the creation paths finally agree about what a headerless
caller is.

Verified the ambient identity is available without any UI: a
daemon-only process resolves it from the vela session on disk, and
`GET /api/workspace/context` answers fully in that state. The
`ambientWorkspaceId` in `collab/workspace-hub-subscriptions.ts` is
unrelated — it is SSE subscription bookkeeping fed from the
disk-persisted selection store, not an authorization input.

Deliberately NOT weakened: `authorizeCreatedProjectWorkspace` still
refuses to let ambient stand in for an explicitly CLAIMED pair, and
`resolveProjectWorkspaceScope` still resolves a persisted binding without
consulting ambient. Both govern cases where something WAS asserted;
headerless is the third case.

What stays refused, because the original branch protected something real
(recvqbeDjAsejl / recvqbklNGDqYY, spec 04 §10): a resource bound to a
workspace the daemon is not currently in; a resource the daemon's own
identity may not mutate anyway (the same
`workspaceResourceMutationAllowed` computation runs); and everything when
the daemon has no signed-in identity to resolve. With no ambient wired
the branch is byte-identical to before.

The ambient snapshot type moves down into
`collab/workspace-resource-mutation.ts` so the gate and the creation
paths cannot drift into two notions of "the daemon's own workspace";
`created-project-workspace.ts` re-exports it and now reuses the shared
context builder instead of its own copy.

* test(e2e): pin that signed out, visible equals unbound equals mutable

「未登录也可以用自己 cli 修改未登录态下的那些 project, 这个一定要保证,
不然就是 P0 事故」

A signed-out client can only SEE unbound projects: the no-scope catalog
(`routes/project/index.ts:2432-2443`) reads no `x-od-workspace-*` at all
and joins through `listUnboundProjects`, so a project any workspace has
claimed never leaks to a caller with no identity to check it against
(spec 04 §10). This pins the other half of that set — everything visible
while signed out stays WRITABLE.

The case runs against a daemon with NO vela session at all, not merely a
request without headers, creates its draft in that same signed-out state
(the real user sequence), asserts the draft is listed by the no-scope
catalog, then renames it, duplicates it, and duplicates it again through
the real `od` binary.

It holds only because `headerlessMutationAllowed` short-circuits on "no
row anywhere" BEFORE it asks for an ambient identity, so it is
mutation-tested against that ordering rather than written red-first:
moving the short-circuit after the ambient check turns it red with
`expected 401 to be 200`.

* fix(collab): make the delete side effect use the gate's own context

Review follow-up (mrcfps). Allowing a headerless caller to mutate a
project the daemon's own identity owns made a latent inconsistency
reachable: `DELETE /api/projects/:id` re-derived its context with
`workspaceProjectContextFromRequest(req)`, which is null for a headerless
caller, so `requestTeamVisibility(..., 'personal')` was skipped while
`dbDeleteProject` still ran. The owner's local row and directory
disappeared and the hub kept serving the resource, so teammates went on
seeing a project that no longer existed — a cross-client data-consistency
bug, worse than the 401 this branch set out to fix.

Threading the context, not narrowing the capability:
`effectiveWorkspaceProjectContext` names the identity a mutation actually
acts under — asserted when the request carries one, ambient when it does
not — and its docblock states the invariant that any SIDE EFFECT must read
from it rather than re-deriving from headers, or the effect disagrees with
the gate that permitted it. The delete now uses it.

The no-context case becomes a refusal rather than a silent skip. It is
unreachable while the gate is intact (a team-bound row is admitted only
for an asserted identity or a matching ambient one), but falling through
is what produced a local-only delete of a still-shared project, so a
future gate change must not be able to reintroduce it quietly.

Audited every other capability this branch newly admits for a headerless
caller. Delete was the only one: `/move` and both share/unshare routes
each guard with their own `if (!ctx) return sendMissingWorkspaceContext(res)`
so headerless never reaches their hub work, and the upload route's
`workspaceProjectContextFromRequest(req) !== null` only shapes a 404 for a
missing project. Rename/duplicate/writeFiles/comment have no hub side
effect.

Adds the requested route regression to
`tests/routes/project-delete-unshares-team-share.test.ts`: real route,
real collab runtime, mutable fake hub, no request headers,
`workspaceContext.lastKnown()` populated — asserting the unpublish and
catalog removal happened and the project is gone locally only after. Plus
the other half: with no ambient identity either, the delete is refused and
nothing is destroyed. Mutation-tested — restoring the header re-derivation
turns the first red with "expected 0 to be greater than 0".

---------

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(web): draw the team wordmark for every tier of a team plan (#6219)

The account row of a team workspace showed the PERSONAL Plus wordmark. Product
ruling: 「团队版的订阅,这里应该都显示 team 的标识」…「产品期望团队从 free 到
max,徽标都显示 team 的那个,个人的还是维持现状不要动」.

The badge names the plan FAMILY, not the tier inside it, and it got that wrong
from both ends. `planBadgeTierForLabel` asked `plus` / `pro` / `max` BEFORE
`team`, and B namespaces team ids by EMBEDDING the personal tier word — so
`team_plus` matched `plus` and the `team` branch was reachable only from a bare
`team` id, the one team tier that happened to work. The free team tier missed
from the other side: B reports an unsubscribed workspace with a null `planId`
and an empty `membershipTier`, an id no different from a personal free account,
so no id can name it.

`planBadgeTierForWorkspace` states the whole rule once, and both surfaces read
it — the rail's account row and the Settings CLI card, which the owner requires
to move together (「设置中的这里应该一样的逻辑」). Team-namespaced ids go through
the shared `isTeamPlanTier`, so the badge cannot drift from the other team
gates; `workspaceType === 'team'` covers the free tier that no id can express.

The plan LABEL is untouched, deliberately: it answers a subscription question,
every user-created workspace in B is team-typed, and reading one as the other
is #146. 免费 beside the `team` wordmark is now the intended pairing — the
family is team, the subscription is not paid.

No interaction, layout, sizing, artwork, or copy changes; no new i18n keys.

* fix(collab): warm the workspace-scoped digest faces on switch and reconnect (#6222)

Every workspace-scoped cache in the daemon keys on the active workspace, so the
instant the user switches, `catalog` and `members` are both a miss and the FIRST
consumer in the new workspace refills them inline on its own request path
(`createPersistentSyncCache` fetches synchronously on a miss). The cost is the
same either way; the only question is who waits for it. A switch is a user
action followed by idle time, so paying it there is free — paying it inside the
first project load or agent run is not.

`PUT /api/workspace/active` now announces a CONFIRMED switch through a new
fire-and-forget `onWorkspaceSwitched` seam, and the daemon warms the two faces
from it. The announcement deliberately does not fire for a rejected or
rolled-back switch: warming there would refill the caches against a workspace
this daemon just refused to move to.

Warming goes through the cache functions, never their underlying fetchers.
`createSwrCache`'s read returns `entry.inflight ?? refresh(key)`, so a warm that
races the very consumer it is protecting joins that consumer's request instead
of issuing a second one. No request is added that would not otherwise be made.

Second half, same root cause: on hub reconnect the gap-closing catch-up read the
team-project and member lists THROUGH those same caches, so an entry that
settled just before (or during) the disconnect still counted as fresh, the diff
concluded "nothing changed", and the one cycle meant to close the gap emitted
nothing — the client stayed stale until an unrelated read happened to miss.
Reconnect now drops those two entries first, and `pollOnce` joins the in-flight
refresh that starts, so the pair still costs one fetch per face.

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(daemon): split AMR workspace proof failure from proof refusal (#6221)

An AMR run died in preflight with "its persisted workspace <id> is
unavailable for the signed-in member". Two causally unrelated situations
shared that one error and one outcome: a directory that answered and
genuinely listed no active membership, and a directory that could not be
read at all. The `.catch` swallowed a read failure into
`{ ok: false, items: [] }`, so a transient vela blip made a user's own
project unrunnable — and nothing downstream could tell the two apart.

Refusal now fails closed under `ProjectWorkspaceScopeRefusedError`
(`PROJECT_WORKSPACE_SCOPE_REFUSED`); an unreadable directory is retried
with bounded backoff and, if it still cannot be reached, the run proceeds
with no workspace, i.e. on the caller's own account wallet. That is a
product decision recorded in the docblock: a hard failure is worse than a
mis-attributed personal charge. The reverse direction, charging a personal
run to a team wallet, stays impossible — no fallback selects another
workspace, and a proven non-membership still refuses.

The decision was also invisible: a full diagnostics export carried no
directory-fetch line, no run-id in the daemon log, and only a generic
AGENT_EXECUTION_FAILED plus prose. Every branch now reports a closed-union
outcome to the daemon log, the run record (via `ApiError.details`, leaving
the wire error code unchanged) and telemetry.

* fix(comments): preserve rejected mutations

* fix(comments): preserve partial send results

* fix(web): keep workspace-scoped data fresh across account and workspace changes (#6225)

* fix(web): keep workspace-scoped data fresh across account and workspace changes

Three defects in one family — workspace-scoped data in the web client going
stale or being read under the wrong identity.

1. The workspace switcher's module-level directory cache had no invalidation.
   `/api/workspace/directory` answers "which workspaces can the SIGNED-IN
   ACCOUNT see", but nothing cleared the cache on an account change and
   `resetWorkspaceDirectoryCache` had no production caller — so signing in as a
   different account kept the previous account's workspace names on screen, and
   kept them confidently, since a non-empty cache also suppresses the loading
   state. The cache is now served only to an identity that can claim it, by
   membership id, which keeps the flash-free reopen it exists for. The
   `coalescedGet` key is identity-scoped for the same reason.

2. App.tsx read `/api/skills` without the caller's workspace headers and never
   refetched on a workspace switch. The daemon is fail-closed on a missing
   `x-od-workspace-id`, so a headerless read is not the unfiltered list — it is
   the list with every workspace-claimed skill removed, including the ones
   claimed by the workspace the user is in. Skills now get the identity headers
   and the same workspace-keyed refresh design systems already had. Launch still
   spends exactly one `/api/skills` request: the read moved from the boot pass to
   the workspace-keyed effect rather than being added alongside it.

3. `PUT /api/workspace/active` already returns the post-switch context, from the
   same producer `GET /api/workspace/context` serves and only after the daemon
   agrees the switch happened. The client parsed that body, discarded it, and
   made every consumer re-read the same data. The shell is now seeded from the
   response; the refresh broadcast still fires, because
   `useProjectWorkspaceScope` listens to it and passive tabs cannot be seeded.
   Switch path: 2 requests -> 1.

* fix(web): discard workspace-scoped reads that land after the identity moved

Review follow-up on #6225 (PerishCode, two threads — both real).

Keying a read by identity stops the wrong identity being SERVED a cached
answer. It does nothing about the other direction: a read issued for identity A
resolves later, and by then the caller may be identity B. Committing that late
answer restores A's data under B — the same staleness this PR removes, arriving
through the back door. A workspace switch is precisely when one read is in
flight and another starts, so reverse-order completion is reachable, not exotic.

Both sites now use one mechanism, `beginWorkspaceScopedRead`: capture the
identity the read is issued FOR, request with that captured context, and compare
against a ref before any commit. Its docblock states the invariant and the two
rules that make it hold (request with the captured context so request and guard
cannot disagree; compare against a ref, never a closed-over prop, which would
always match and guard nothing). It is the cross-component form of the
`requestEpochRef` ordering guard `useWorkspaceContext` already applies to its
own read.

- `refreshSkills` and `handleSkillsChanged` (App.tsx): a slow read for the
  workspace the user left no longer overwrites the current catalog. The
  registry-ready gate is intentionally skipped on a discarded response — it is
  not an answer about the current identity, and the newer read marks it.
- `loadWorkspaceDirectory` (EntryNavRail.tsx): an in-flight account-A read that
  lands after the rail moved to account B no longer repopulates either the
  module cache or the visible list. This was the sharper of the two, because it
  wrote into module scope and so persisted past the render that caused it.

Request counts are unchanged (startup 4, switch 1): the guard discards
responses, it does not issue requests.

* fix(web): make computing a workspace identity total, and key the skills trigger on it

Fixes the `Web workspace tests` failure 886f11b24 introduced. CI reported all
555 files and 5583 tests PASSING and still exited 1:

  Unhandled Rejection
  TypeError: Cannot read properties of undefined (reading 'canShareProjects')
   ❯ workspaceIdentityCacheKey src/collab/useWorkspaceContext.ts:112:32
   ❯ beginWorkspaceScopedRead src/collab/useWorkspaceContext.ts:159:20
   ❯ src/App.tsx:1770  ❯ src/App.tsx:1799  ❯ commitHookEffectListMount
  originating in tests/components/App.amr-plan-tier.test.tsx

`workspaceIdentityCacheKey` dereferenced `context.permissions` unguarded. That
survived while it was only called during render (a throw there fails loudly, in
the right place) and inside `fetchSkills`'s own try/catch. 886f11b24 put it
behind `beginWorkspaceScopedRead` in an async function launched with
`void refreshSkills()`, so a partial context turned a synchronous programming
error into an unhandled rejection — invisible to every assertion, fatal to the
process.

`permissions` is required on the contract, but API JSON reaches these objects
through an unchecked cast, so a partial context is reachable at runtime and not
only in fixtures. Computing an identity must therefore be TOTAL: a partial
context is its own cache partition, never an exception.

Second, independent defect, found while fixing the first: the skills trigger was
keyed on `workspaceId` while the commit guard compares the full identity digest.
A guard that discards without guaranteeing a successor is worse than the
staleness it replaced — two accounts in one shared team workspace differ only by
membership, so account A's pending read would be discarded for B while the
unchanged workspace id suppressed B's replacement read, leaving the functional
registry loading forever. The trigger now uses the same digest the guard
compares, so every transition that invalidates a response also starts its
successor.

Both are covered by tests that go red without the fix:
- tests/collab/workspace-identity-key-is-total.test.ts (new) — identity is total
  for null, partial and complete contexts, including a late commit-time guard.
- tests/components/App.skills-workspace-scope.test.tsx — a member-id-only change
  must start AND complete a successor read.

---------

Co-authored-by: lefarcen <ontf116@gmail.com>

* feat(landing): publish current pricing snapshot

* fix: durably deliver enterprise leads

* test(web): update ProjectView comment-status suites to the workspace-scoped signature (#6233)

`feat/workspace-team`'s `Web workspace tests` lane is red at the branch tip: 3
files / 4 tests, all comment-status assertions. Not a flake and not attributable
to any open PR — I had to prove twice while landing #6225 that a red lane was
inherited rather than mine, which is the cost a red base imposes on every
downstream PR.

Cause: `1c15574c2 fix(comments): preserve rejected mutations` gave
`patchPreviewCommentStatus` a fifth argument — the acting workspace context — so
the daemon can authorize the mutation (`routes/project/comments.ts`, same
commit). It updated `FileViewer.test.tsx` and added `registry.test.ts` coverage
but left three `ProjectView.*` suites on the four-argument signature.

The tests were stale, not the code. Checked before editing, because "update the
tests" is the wrong move if the assertions were right:

- Every asserted call still happens, unchanged. The failures are pure arity
  mismatches: `api-empty-response` expects `(…, 'failed')` and receives
  `(…, 'failed', null)`; likewise `needs_review` in run-cleanup and `applying`
  in run-isolation. No status transition changed.
- `1c15574c2` shipped the new contract WITH tests —
  `describe('preview comment scoped mutations')` asserting `x-od-workspace-id`
  reaches the PATCH. The fifth argument is deliberate and covered.
- The parameter is optional and additive; headers are spread only when a
  context is present.

Also restores two assertions that had gone silently vacuous. A four-argument
matcher cannot match a five-argument call, so `run-isolation`'s two
`not.toHaveBeenCalledWith` assertions could never match once the signature grew
and passed unconditionally — coverage actively lost, not merely failing. Both
pass with the correct arity, which independently confirms the new code does not
write `needs_review` after a canceled+done run and does not reset a queued
send's own comment to `open`.

The fifth argument is matched as an explicit `null` rather than
`expect.anything()`, which rejects null/undefined and would have re-broken the
positive assertions and re-vacuated the negative ones.

Tests only — no source file is touched.

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(collab): make the local pin the only workspace-selection truth (#6230)

* test(collab): pin the one-account-two-clients workspace scope behaviour

Executable evidence for the account-level Active Workspace investigation.
Models vela exactly as its source behaves at origin/main 2155a969f:

- active_workspace_selections is keyed by app_user_id alone
  (db/schema/public.hcl:792), so an account has exactly ONE selection.
- GET /api/v1/workspaces/current discards a ?workspaceId= hint
  (services/api/src/workspaces/routes.ts:166-176, and vela asserts it by
  name in services/api/test/workspaces-personal.test.ts:480).
- GET /api/v1/workspaces is scoped by app_user_id, not by the selection.

What it establishes: OD's local pin DOES hold against a competing client
(resolvePinnedWorkspace), so the switch does not 404. The damage is that
the losing client's context degrades to a directory synthesis - planId
null and a 0/0 seat summary, which derives isSeatFull: true - and its
context read gains a second, fallible round-trip the winning client
never makes.

No behaviour change.

* fix(collab): make the local pin the only workspace-selection truth

Picking a workspace is a local action, but `PUT /api/workspace/active`
gated it on mutating ACCOUNT-scoped state in vela: it PUT the account-level
active workspace first and failed the user's click with 502
`workspace_switch_rejected` when that write did not take, then rolled back
both the local pin and the backend switch if the follow-up context read
disagreed.

That backend row is keyed by app user alone, so it can only ever name ONE
workspace for an account. One account routinely runs several clients in
different workspaces, so every switch yanked the other clients' server-side
scope, and whichever client did not write the row last was answered for the
wrong workspace — forcing OD to synthesize its context from the membership
directory, which carries no billing plane. That client saw planId null and a
0/0 seat summary, and a 0/0 summary derives isSeatFull: true, so a workspace
with free seats read as FULL and blocked inviting. It also turned that
client's context read into two round-trips, the second of which could blip
and leave it with no context at all.

vela #1179 (1ba3eebcb, deployed to test 09:57Z) taught
`GET /api/v1/workspaces/current` to honour `x-vela-workspace-id`, the
per-request workspace scope vela already accepted on its resource plane,
billing scope routes and the Link gateway. So:

- `fetchCurrent` sends that header and drops the `?workspaceId=` query
  param, which vela ignores by design and asserts it ignores — it only ever
  made this call look scoped.
- `PUT /api/workspace/active` no longer calls vela. The membership
  directory it already reads is the authorization; once that confirms an
  active membership the switch cannot fail, so the 502 gate and the
  rollback are gone along with `selectWorkspace` and `putCurrentWorkspace`.
- An unconfirmed context read is no longer treated as a refusal: the route
  answers from the directory entry it already validated instead of
  reverting the user's choice, mirroring the invariant
  `resolvePinnedWorkspace` already follows in the same module.

Verified against the deployed test backend, GETs only: an explicit
workspace returns that workspace (not the account row), a workspace the
caller is not a member of is refused 404 `workspace_member_required`, and
both an absent and a blank header keep the previous account-level
behaviour. The URL query hint is still ignored.

vela's `active_workspace_selections` table and its write path are left
alone; deprecating it is a separate decision and #1179 kept omitting the
header backward compatible.

Two comments that asserted the now-false invariant are corrected.

* fix(collab): never answer a switch with a workspace the pin no longer holds

Review catch (mrcfps): `workspaceContext.current()` is not a passive read.
The vela provider's `resolvePinnedWorkspace` does its OWN fresh directory
lookup and, when that confirms the pinned workspace is gone, clears the pin
and re-pins a recovery workspace so a removed member is not locked out of
everything. The route's directory read can meanwhile have been served from
its 5s cache and still list that workspace.

The previous branch state read `context.workspaceId !== workspaceId` and
treated the provider's recovery as a merely-unconfirmed read, synthesized the
stale cached entry, and returned 200 claiming the requested workspace while
`activeWorkspace.get()` had already become the fallback. That shipped this
PR's own thesis — the local pin is the only selection truth — as a violable
invariant, and the user-visible symptom is the switch appearing to succeed
and then snapping back on the next context poll.

The pin is now re-read AFTER `current()` resolves. If the provider moved it,
the route reports `404 workspace_no_longer_available` for the workspace the
user asked for, never claims it, and does not warm its caches. The recovery
pin is deliberately left in place: reverting it would restore the very
"signed out of every workspace" state it exists to prevent.

The directory entry that authorizes the switch is also now required to be a
LIVE membership (`memberStatus === 'active'`, lifecycle not deleted) — the
same predicate the provider uses. Matching on id alone let a listed-but-
removed membership authorize a switch AND get synthesized into the response.

Tests wire the real provider against the production cached directory fetcher
over one shared pin store, exactly as server.ts does, so the cached and fresh
reads genuinely disagree. Disabling the new pin re-read turns "never claims a
workspace the pin no longer holds" red and nothing else.

---------

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix landing pricing controls

* fix(daemon): resolve the AMR scope-telemetry app version in its own scope (#6240)

Every AMR run in 0.16.2-beta.148 died at spawn with
`spawn failed: appVersionForCapture is not defined`.

`appVersionForCapture` is a local of
`createFinalizedMessageTelemetryReporter`. #6221's
`amr_workspace_scope_resolved` capture called it from `startChatRun`,
~7,500 lines and one function away, so the name resolved to nothing.
The callback runs while building the agent's launch env, inside the
spawn `try`, so the ReferenceError surfaced as the generic
AGENT_EXECUTION_FAILED spawn failure for 100% of AMR runs.

`design.getAppVersion` is the accessor that scope actually has, and it
is the same one the unreachable helper falls back to, so the reported
value is unchanged.

A scope guard covers the class rather than the instance: it runs the
checker over `server.ts` with `@ts-nocheck` stripped and fails on any
identifier that reaches runtime unbound. It flagged one sibling, fixed
here too — the critique orchestrator's `skill` label read
`effectiveSkillId`, a local of `composeDaemonSystemPrompt`. A `typeof`
guard on an undeclared name does not throw, so instead of crashing that
label has silently been `undefined` for every run since #1485.

`@ts-nocheck` on `server.ts` is the enabling condition for both: an
undefined identifier in this file reaches users without any tool
objecting. Removing it is a separate project — 29 daemon files carry it.

* fix(web): isolate workspace identity snapshots

* test(e2e): move the server.ts identifier-scope guard into the merge gate (#6244)

`apps/daemon/tests/server-identifier-scope.test.ts` landed with the
0.16.2-beta.148 AMR spawn fix (#6240) but protected nothing: `ci.yml`'s
"Daemon workspace tests" step runs exactly one file
(`tests/project-watchers.test.ts`), so no other daemon suite executes in
any workflow. The guard sat outside every merge-gate lane.

Widening the daemon lane is not available as a local fix — that exact
command string is pinned by `workflowRunsOnlyAllowedDaemonTest` in
`scripts/check-certain-exempt-consumption.ts`, which conditions the
`trae-cli.test.ts` docs-consumption exception on the lane running nothing
else. Touching it is a coupled scope-policy change.

So host the guard where a lane already runs and the wiring is itself
guarded:

- Any change under `apps/daemon/src/` matches `certain-daemon-core`
  (`scripts/scopes.ts`), whose effects include
  `ui_p0_validation_required`; `run_e2e_vitest` is
  `isFull || web_tests_required || ui_p0_validation_required`. A
  `server.ts`-only change therefore arms `E2E Vitest` even at the merge
  queue's `certain` threshold, and an unresolved file list escalates
  fail-closed to full.
- The `daemon core boundary` guard (`scripts/lib/guard/scope.ts`) asserts
  `ci.yml` still contains both `run_e2e_vitest == 'true'` and
  `pnpm --filter @open-design/e2e test`, and runs in the always-on policy
  floor — so the premise cannot rot silently.

The check only ever reads `server.ts` as text (no daemon boot, no
fixtures), and reading another app's tree as a repository resource is
what root `AGENTS.md` sends to `e2e/tests/` — the same reason
`e2e/tests/critique-coverage.test.ts` lives there. Behavior is
unchanged, including both self-checks that keep the guard from going
vacuous.

Verified red on the pre-#6240 `server.ts`: exit 1 naming
`appVersionForCapture (line 9261)` and `effectiveSkillId (lines
9657/9658)`; green (exit 0) on the fixed file.

* fix(collab): align unbound project mutation identity (#6231)

* fix(web): name the caller when a run starts before its project scope resolves

Sending the 「水面焦散」 example prompt from Home failed instantly on a team
workspace:

    daemon 401: {"error":{"code":"WORKSPACE_CONTEXT_REQUIRED","message":"workspace context is required"}}

with `run_id: n/a`, because the refusal lands before the run is created. That
string is produced in exactly one place in the repo — the `!createResp.ok`
branch of the `POST /api/runs` create fetch in `providers/daemon.ts` — so the
refused call is run creation, gated by `enforceWorkspaceResourceMutation
('project', …)`.

The cause is client-side. Home creates the project WITH the caller's workspace
headers, so #6201 binds it to the team workspace; the follow-on auto-send then
went out with NO headers, because it took its identity from the PROJECT's
workspace scope — an async read still in flight when the auto-send fires (its
gate is `messagesInitialized` + `activeConversationId`, never the scope). Every
other project write in `ProjectView` (`patchProject`, `uploadProjectFiles`, the
comment mutations) already asserts the caller's own context; run creation was
the one write that did not.

INVARIANT, now named in `runWorkspaceIdentity`: a send names its caller while
the project's binding is still UNREAD, and never overrides an answer the daemon
actually gave. Deliberately not a blanket `?? caller` — every answered or failed
state keeps asserting nothing, because for them a headerless request is judged
on daemon-owned state while an asserted one is judged on client-controlled
headers:

  - `unbound` — `headerlessMutationAllowed` short-circuits on "no row anywhere"
    BEFORE asking for an identity, so asserting would turn a working 200 into a
    403 and break 「未登录也可以用自己 cli 修改未登录态下的那些 project」.
  - `unavailable` — the daemon's `resolveProjectWorkspaceScope` returns this
    both for an unreadable directory AND for a directory that answered with no
    active membership, i.e. a proven "you were removed".
  - a FAILED scope read — this identity also picks the wallet the AMR pre-run
    balance gate prices against, and guessing the caller's team wallet with the
    binding unknown blocks a send the daemon would have answered (the dead
    button 21f452ffe undid).

Second consequence of the same null, fixed by the same one-liner: with no
resolved scope, `checkAmrBalanceGate` was called with `undefined` and priced the
run against the ACCOUNT wallet instead of the team's. The red spec observes
that directly rather than arguing it.

`ProjectView.run-isolation.test.tsx` mocks `useProjectWorkspaceScope` with a
hand-written factory that enumerates every export, so the new export is added
there too — without it, adding one export breaks all 64 tests in that file with
"not a function" at render.

* fix(web): narrow the run-identity fallback to a project's first unread binding

Review catch (nettee): keying the fallback on `loading` served two opposite
situations, because `useProjectWorkspaceScope`'s trailing transition guard
re-enters `{ loading: true, scope: null }` whenever the resolved caller identity
stops matching — and every workspace switch does that:

  - first read, nothing known    -> the caller is the only identity available
  - revalidating a KNOWN binding -> the caller's current workspace is a guess
    that contradicts an answer we already have

So a send issued during a workspace switch asserted the workspace the user had
just moved TO for a project still bound to the one they moved FROM. Both
consumers break, and differently: `POST /api/runs` can 403 because
`enforceWorkspaceResourceMutation` still finds the project row under the old
workspace, and `checkAmrBalanceGate` preflights the new workspace's wallet —
worse than the `undefined` this fix was written to remove, because a wrong
wallet looks like a real answer.

`ProjectWorkspaceScopeState` now carries `initialLoadPending`, reported by the
hook rather than inferred from `loading`: true from mount until the first settled
answer (a scope OR a failure) for the current `projectId`, false for that
project's whole lifetime afterwards, and reset when `projectId` changes because a
different project genuinely has not been read. `runWorkspaceIdentity` keys the
fallback on that bit alone, and its docblock names which of the two loading
reasons it serves so the distinction cannot erode.

The field is required, not optional, so every construction site has to state
which case it is. Two pre-existing assertions in
`useProjectWorkspaceScope.test.tsx` now document the distinction they always
exercised: a project A->B rerender reports pending (B is unread), while a
same-project identity change reports not-pending — the exact window this fixes.

* fix(collab): align unbound project mutation identity

Allow an identified caller to mutate a project that no workspace has claimed, matching the existing headerless and unbound-resource behavior without persisting a new binding. Name the caller on Home auto-send while keeping AMR preflight scope derivation unchanged.

* fix(web): refresh the settled run billing scope

Track the project billing context in both callbacks that read it so an auto-send after scope settlement cannot preflight through a stale unscoped closure. Add a regression whose run identity stays referentially stable while the billing scope changes.

* fix(web): stop asserting disproved run scope

* fix(web): guard workspace-scoped catalog reads (#6249)

* fix(web): guard workspace-scoped catalog reads

* fix(web): guard remaining plugin catalog races

* fix(web): align design catalog guard scope

* fix(landing): show introductory team totals

* fix(daemon): label critique with project skill (#6251)

* fix CI scopes across file renames (#6252)

* fix(ci): preserve scopes across renames

* fix merge queue rename ceiling detection

* test raw merge compare responses

* fix(landing): make contact sales KV canonical

* fix(web): preserve Cloud sign-in during outages (#6274)

* fix(collab): allow shared project viewer comments

* fix(web): keep readonly comment creation available

* feat(web): personal/team project labels + collapse chat for shared non-owners (#6294)

* feat(web): rename sidebar project lists and collapse chat for shared non-owners

Workspace home left rail now labels personal vs team project lists
("Personal projects" / "Team projects", 个人项目 / 团队项目) across all
locales. Opening a confirmed shared team project that the viewer did not
create defaults the chat pane to collapsed so the design is primary;
owners and personal projects keep chat open, and expanding chat stays
sticky for the visit.

* fix(web): use effective ownership for shared non-owner chat default

Default-collapsed chat was latching on raw isOwner, so a catalog-confirmed
owner whose collab status was still missing ownerMemberId looked like a
non-owner and permanently collapsed chat. Expose isEffectiveOwner and
isSharedNonOwner from useProjectCollab (catalog + session-created + status)
and gate the sticky collapse path on the confirmed non-owner signal only.

* fix(web): stop narrow chat error cards stacking titles vertically (#6295)

Move the long Open Design Cloud switch CTA into UserActionCard footer
actions (same shell as run-recovery), add a card-width container query so
head actions reflow when ChatPane is narrow, and soften title wrapping so
CJK copy no longer renders one character per line.

* fix(workspace): make resource authority explicit (#6287)

* fix: make workspace authority explicit across resources

* fix(web): keep new team projects writable during scope load

* fix: close workspace authority review gaps

* fix(web): retry rejected project auto-send

* fix: propagate workspace scope to project consumers

* fix: cache project read workspace authority

* fix(web): stabilize project scope revalidation

* fix: cache project collaboration status authority

* fix: cache collaboration status owner reads

* fix: revalidate media wait tool authority

* fix: rehome member comments on local threads

* fix(web): stabilize project resource authorization

* fix(web): prioritize settled project authority

* fix(web): consume queued home auto-send handoffs

* fix: revalidate dirty comment pulls

* fix: isolate mutation authority reads by session

* fix: cache project presence read hot path

* fix(web): stop unauthorized project background writes

* fix(web): require settled scope for project background work

* fix(web): scope remaining project reads by workspace

* fix(web): scope remaining project resource consumers

* fix(web): partition project caches by full authority

* fix(daemon): pin run workspace billing scope

* fix(daemon): lease workspace read authority

* fix(daemon): keep presence reads on the hot path

* fix(web): refresh deck thumbnails across workspace scope

* fix(daemon): revoke stale unshared team mirrors

* perf(daemon): cache stale mirror read guards

* fix(web): gate deep-link reads on project workspace

* fix(web): keep deep-linked project tab scoped

* fix(web): preserve project routes during onboarding

* fix(web): keep project previews warm across file-list toggles

* perf(daemon): refresh presence without cold reads

* fix(web): keep retained preview iframes connected

* fix(web): reuse exact HTML thumbnail sources

* fix(web): settle read-only empty project chat

* fix(daemon): scope CLI run transports by workspace

* test(web): align preview cache refresh semantics

* fix(web): require positive shared viewer ownership proof

* fix preview auto-fit measurement feedback

* preserve preview fit during annotation freezes

* preserve preview measurement semantics on refresh

* stabilize preview overflow measurement test

* test design systems move exclusively to team

* fix design systems move exclusively to team

* test shared project title scope convergence

* test(daemon): cover project command workspace scope

* fix(daemon): scope project command transports

* fix shared title test project state type

* fix(daemon): preserve shared project catalog title

* fix(daemon): keep foreign mirrors creator-unattributed

* fix(web): recover first shared project conversations

* fix(web): fence conversation recovery generations

* test(web): verify conversation recovery cleanup fence

* UI polish batch: cloud sign-in, onboarding, composer menu (#6281)

* UI polish batch: cloud sign-in, onboarding, composer menu

Sign-in surfaces (rail callout, balance-gate dialog, onboarding) had
drifted from the visual system and lost some redundant chrome; this
batch tightens them up alongside a couple of composer/design-files
reorganizations that were sitting in the same working tree:

- Rail's signed-out callout: reorder copy above the login pill, give
  the pill an icon + accent-green label, restore the Escape hatch
  the pill had before it lost its close affordance.
- AmrBalanceDialog (insufficient-balance / signed-out gate): add a
  full-bleed art banner above the copy, drop the now-redundant icon
  badge, tighten spacing/shadows to match.
- Onboarding cloud landing: switch from a single centered column to
  a two-column layout (form pane + full-bleed art panel), move the
  language switcher into the footer, swap the local-CLI/BYOK links
  for icon Buttons, and add a dismiss control to the activation-retry
  hint.
- Composer "+" menu: fold 插件/设计百宝箱 back in as hover-expand
  submenu rows instead of standalone quick pills above the input.
- Design files panel: move new-document/upload actions out of the
  always-visible toolbar into the empty-state actions only.
- Message center: drop the redundant header close button (backdrop
  click and Escape still dismiss it) and restyle the list from
  bordered cards to a divider-separated list.
- New `key` / `robot` / `translate` / `arrow-right` icons; language
  menu switches from the `languages` glyph to `translate`.
- Tab launcher popover reverts to the glass material; language
  dropdown gets a max-height + scroll so long lists don't blow out
  the popover.

Rebased the composer-quick-pill removal onto this branch's own
accessibility work on those pills (opener-based focus return via
`ComposerStandalonePanel`) rather than reverting it — the "next step"
card still drives the same standalone-panel API, just without a pill
to hand focus back to. Also updated/removed the tests that pinned the
now-removed surfaces (quick-pill popup contract, message-center close
button, onboarding top bar, design-files toolbar upload trigger) so
none of this lands with a newly-red suite.

* perf(web): optimize onboarding cloud artwork

* fix(web): invalidate project file reads after mutations

* fix(web): disable readonly project starter mutations

* fix(web): preserve focus for standalone composer panels

* test(e2e): require explicit workspace scope

* fix(web): invalidate file reads on project events

* test(e2e): seed explicit settings workspace

* test(e2e): align manual edit with current inspector

* fix(web): refresh deployments when share opens

* test(e2e): follow composer design system entry

* fix(web): keep local project creation workspace-independent

* fix(web): preserve scoped auth recovery state

* test(e2e): follow current entry settings flows

* fix(web): preserve AMR retry across settings

* test(e2e): model explicit personal workspace recovery

* test(e2e): stabilize AMR logout settings recovery

* fix(projects): return workspace scope on create

---------

Co-authored-by: lefarcen <935902669@qq.com>

* fix(workspace): preserve scoped projects after main sync (#6320)

* fix(daemon): surface AMR OpenCode stall context (#6040)

* fix(daemon): surface AMR OpenCode stall context

* fix(daemon): accept runtime-omitted OpenCode diagnostics

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

* fix(ci): make static gate runner-independent (#6039)

---------

Co-authored-by: PerishFire <39043006+PerishCode@users.noreply.github.com>

* test(e2e): parallelize restoration group (#6169)

* test(e2e): parallelize restoration group

* test(e2e): identify conversations from persisted prompts

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

* test(e2e): wait for conversation persistence

---------

Co-authored-by: Looper <looper@noreply.github.com>

* feat(landing-page): sharpen the Codex agent page for "codex ui" intent (#6200)

The /agents/codex-design/ page ranks #1 for "codex design" but only ~6th
with a 1.3% CTR for "codex ui", a query that grew from 30 to ~90 daily
impressions this month. A live SERP check shows the intent is mostly
"build UI with Codex" (our intent), with a minority looking for the Codex
app's own interface.

- Retitle to "Codex Design: Build UI with OpenAI Codex" (title + H1).
- Lead the frontend section with the query phrase and an answer-shaped
  opening paragraph, so the section is extractable for AI answers.
- Add a disambiguation paragraph separating the Codex app's own UI from
  the UI you build with Codex, and route the former back to the main line.
- Add two FAQ entries covering both readings; they also feed the FAQPage
  schema, which already earns a text snippet on this query.

Applied across all 11 shipped locales (en, zh authored; the other nine
translated with the page's terminology preserved).

Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>

* Add Clone Audit plugin (#5687)

* Add Clone Audit plugin

* Update open-design.json

* fix: make clone audit skill reusable

* fix: sanitize clone audit provenance path

* fix(e2e): bound tools-dev runtime lifecycle (#6204)

* test(e2e): refresh full-pool UI fixtures (#6211)

* fix(web): allow skipping required chat questions (#6177)

* fix(web): allow skipping required chat questions

* fix(web): auto-skip unanswered chat questions

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>

* docs(tools-pack): add build cache contract (#6207)

* docs(tools-pack): add build cache contract

Document the tools-pack build-graph cache as a contract: the exact-match
acceptance model, determinant rules, materialization-time parameters, the
signing boundary, and fail-closed confidence grading.

The rules come from an audit of all 11 cache nodes. Two are written down
because nothing currently enforces them: a node key must carry the key of
every upstream node it consumes, and a node key must not restate a list that
already exists as a constant.

Known low-confidence points are declared explicitly so fail-closed grading
stays checkable rather than becoming a verbal convention.

* docs(tools-pack): clarify cache materialization exceptions

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

* docs(tools-pack): document NSIS base version scope

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

---------

Co-authored-by: Looper <looper@noreply.github.com>

* docs(landing-page): add 0.16.0 release post, localize and re-cover 0.15.1 (#6236)

* docs(landing-page): localize 0.15.1 post, fix its cover, add 0.16.0 release post

The 0.15.1 blog post shipped English-only and with an off-brand cover: a
Fauvist oil painting overlaid with large display type, which reads nothing
like the warm editorial illustrations every other release post uses. It also
pointed `socialImage` at a 309KB JPEG while a WebP sat unused beside it.

Regenerate the cover in the established house style (cream paper ground,
sage/terracotta watercolour washes, pencil sketch resolving into a calm
product surface), drop the stray JPEG, and add the full ten-locale i18n block
so /zh/, /ja/, /ko/, /de/, /fr/, /ru/, /es/, /pt-br/, /it/ and /tr/ read in
their own language instead of falling back to English.

Also add the missing 0.16.0 "Reliable Delivery" post with a cover plus two
inline figures in the same style, localized across the same ten locales. The
0.16.1 patch is a single preview fix, too thin to carry its own page, so it
lands as a closing section of the 0.16.0 post and the download CTA points
there.

* docs(landing-page): fix doubled apostrophes and locale copy in release posts

Three defects surfaced reviewing the localized bodies.

The French, Italian and Turkish bodies rendered every apostrophe twice —
"aujourd''hui", "all''interno", "0.16.1''i". The `''` escape belongs to
single-quoted YAML scalars, but `bodyHtml` is a block scalar, where content is
literal. 173 occurrences across the two posts.

Korean used Japanese corner brackets and Brazilian Portuguese and Turkish used
French guillemets. All three take curly double quotes.

The Chinese copy read as translation rather than prose. "标签" opens the release
line as a UI label, not a version tag; "抢画框", "编辑策展位" and "安静修复" are
literal renderings that say nothing in Chinese; "画廊" is not the term the
product itself uses for a gallery, which keeps the English word. The tag fix
also lands on 0.10.0 through 0.15.0, which share the phrasing — leaving the
series half-corrected would read worse than either state alone.

* docs(landing-page): drop the leading tag word from release post openings

Every release post opened with the word for a git tag before the version
identifier — "Tag open-design-v0.16.0", "Etiqueta …", "タグ …", and in Chinese
"标签", which reads as a UI label rather than a version. The word carries no
information the identifier does not already carry, and in several languages it
lands as a category noun the sentence never uses again.

Drop it across all nine release posts and every locale, in both the summary and
the opening line, leaving the version identifier to open the dateline. Spanish
and Brazilian Portuguese lose the feminine agreement that referred back to the
removed noun; Turkish loses the trailing "etiketi" apposition for the same
reason.

* docs(landing-page): correct the 0.16.1 interval in the 0.16.0 post

0.16.0 was published 2026-07-22 14:42 UTC and 0.16.1 on 2026-07-23 16:11 UTC —
25.5 hours apart. "Two days later" overstated it, and the same wording had been
carried into all ten localized bodies, so every published route repeated it.

---------

Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>

* feat(integrations): add the local Open Design Cloud flow for Codex (#6055)

* feat(daemon): expose Vela login through CLI and MCP

* feat(mcp): add interactive local brief card

* feat(packaged): bootstrap local MCP headlessly

* feat(daemon): secure local BYOK credential profiles

* feat(cli): manage secure BYOK profiles

* feat(web): store BYOK keys in secure profiles

* fix(runs): deduplicate cloud generation requests

* feat(daemon): add end-to-end plugin observability

* fix(pack): include native PTY runtime in desktop packages

Lazy-load node-pty in the daemon so missing native binaries only
disable Terminal and interactive Keychain operations instead of
crashing startup.

Ship and validate target prebuilds in macOS and Windows packages.
Repair the macOS spawn helper before signing, and invalidate stale
Windows packaging caches.

Cover the packaged PTY path in macOS and Windows smoke tests. Require
Vela CLI for every beta desktop build target.

* fix(mcp): localize and stabilize plugin briefs

Keep collect and confirm on one MCP Apps card, report intrinsic size changes,
and remove the fragile widget-state fallback.

Localize brief forms, summaries, status, and public errors from the current
request or Host locale while preserving stable protocol ids and selections.

Keep internal runtime identifiers out of user-facing MCP guidance.

* fix(packaged): register live Studio URLs with daemon

Register the packaged web sidecar loopback origin over namespace-scoped daemon
IPC after dynamic binding.

Invalidate install-info by the live web port and remove the second MCP URL cache
so run and project responses use the current Studio address.

Validate loopback origins and cover registration, rebind, and cache refresh.

* fix(mcp): stabilize brief confirmation lifecycle

Keep confirmed briefs locked after Host publication failures and retry only the cached follow-up message instead of confirming twice.

Replace blank initialization with a compact loading state, accept immediate and delayed tool results, and bump the MCP Apps resource to v3 so Codex does not reuse the stale card.

Add an executable JSDOM Host bridge harness covering double-submit, publication retry, delayed results, and repeated draft notifications. Validated with the full daemon suite, workspace typecheck, and guard.

* fix(mcp): prevent brief app resize feedback loops

Replace the self-observed intrinsic-height loop with deduplicated body measurements so Codex MCP app iframes do not repeatedly resize themselves and fail sandbox startup.

Bump the brief resource URI to v4 to avoid cached v3 cards and cover the regression with the focused brief-app spec. Validated with daemon tests/build, workspace guard/typecheck, a verified beta DMG, and a direct packaged MCP resource smoke.

* fix(mcp): use the final Open Design plugin identity

Make open-design the only accepted external plugin ID across MCP tools, run attribution, artifact version origins, and telemetry. Rename the Brief MCP App resource to v5 and reject the unpublished old ID instead of aliasing it.

Validated with 171 daemon tests, 5 web tests, daemon/web typechecks, and guard.

* chore(pack): bump Vela CLI to 0.0.27

* chore(nix): refresh pnpm dependency hashes

* fix(mcp): honor host and plugin request contracts

Respect independently advertised MCP Apps capabilities and prefer the Codex-native follow-up bridge so confirmed briefs render as readable user turns instead of context-only messages. Bump the brief resource to v6 to invalidate stale cards.\n\nMark Vela login and account probes as open-world interactions, and require stable request IDs for attributed generation so lost responses cannot create a second logical run.

* fix(cli): support keyless BYOK profiles

Add an explicit --no-api-key save mode for local and self-hosted providers while keeping credential-bearing profiles stdin-only. Reject missing or conflicting credential modes and cover both paths in the CLI tests.

* fix(pack): replace headless owner when desktop opens

Advertise headless packaged runtimes as having no visible window, then shut them down before a normal desktop launch continues instead of treating SHOW as a successful focus.

Validated with the focused packaged headless and launcher lifecycle tests (20 passed).

* fix(pack): clean up failed headless startup

Acquire the headless identity, sidecars, and IPC server through one failure-atomic lifecycle so later bootstrap errors close every previously acquired owner in reverse order.

Cover MCP-install and web-identity publication failures; validated with 22 focused tests and the packaged typecheck.

* fix(byok): remove raw provider request contract

Expose only daemon-owned BYOK profile references from shared run types and the web transport, while retaining the daemon's fail-closed rejection for legacy or untyped raw credential payloads.

Validated with 76 web provider tests plus web and contracts typechecks.

* fix(daemon): preserve interrupted run idempotency

Hydrate queued or running durable requests as DAEMON_RESTARTED failures before idempotency reuse, preserving the original request and workflow mappings instead of creating a second cloud operation.

Share the restart transition with boot reconciliation and expose synthetic terminal events to reattaching clients. Validated with 48 focused tests and the daemon typecheck.

* fix(byok): roll back failed credential metadata writes

Snapshot secure-store values before mutation and compensate create, update, and delete operations when profile metadata cannot be persisted, preventing orphaned or lost credentials.

Validated with nine focused credential-service tests and the daemon typecheck.

* fix(web): reconcile selected BYOK profile metadata

Treat the daemon's selected secure profile as authoritative for protocol, endpoint, model, and API version so CLI profile edits cannot leave the browser showing stale execution settings.

Validated with 66 config tests and the web typecheck.

* fix(byok): migrate legacy credentials before cleanup

Import each supported legacy browser credential into a stable daemon-owned secure profile before removing plaintext copies. Keep the original browser record on secure-store or unsupported-provider failures and surface a Settings recovery path.

Cover successful migration, duplicate projections, and injected secure-backend failure while preserving existing Bedrock downgrade behavior in memory.

* fix(byok): secure Windows profiles with DPAPI

Dispatch native Windows BYOK profiles to a CurrentUser DPAPI backend rooted under the resolved daemon data directory. Secrets enter PowerShell only through stdin, encrypted blobs stay separate from non-secret profile metadata, and API responses remain secret-free.

Add platform dispatch coverage, a Windows-only real DPAPI round-trip test, and the packaged Windows smoke path for create, resolve, response-redaction, and delete behavior.

* fix(web): preserve config writes during hydration

* fix(byok): persist secure credentials across entry flows

* test(byok): cover secure onboarding persistence

---------

Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: lefarcen <935902669@qq.com>

* test: strengthen E2E coverage and fixtures (#6150)

* test: strengthen e2e coverage and fixtures

* test: allow artifact route when starting conversation

* test: keep e2e count assertions stable

Use the shared stable-count helper for no-request and duplicate-event windows so delayed async events cannot escape the assertions.

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

* fix: add e2e assertion import extension

Use the explicit runtime extension required by the e2e NodeNext typecheck.\n\nGenerated-By: looper 0.11.0 (runner=fixer, agent=codex)

* fix: preserve design system provenance and settle e2e assertions

Keep disabled design systems available to project recovery and provenance while filtering only runtime injection. Make hover and cancellation checks observe their full settled windows.

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

* test(e2e): preserve unique restoration run IDs

Pass the restoration fixture's run ID prefix through routeSuccessfulRuns so each mocked request receives a distinct run identity.

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

* test(e2e): stabilize agent and async count fixtures

Route restoration agent requests through the shared JSON and SSE fixture, capture autosave baselines after a quiet period, and observe onboarding cancellation for a bounded stable window.\n\nGenerated-By: looper 0.11.2 (runner=fixer, agent=codex)

* fix: honor disabled design systems in daemon runs

Filter disabled persisted project selections at prompt and analytics boundaries, and keep restoration run fixtures unique.\n\nGenerated-By: looper 0.11.2 (runner=fixer, agent=codex)

* test(e2e): shorten cancellation duplicate check

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

* fix(daemon): continue stalled post-tool sessions (#6237)

* fix(daemon): continue stalled post-tool sessions

* fix(daemon): separate post-tool continuation budget

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

* fix(daemon): preserve retry telemetry budget

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

* fix(daemon): align retry failure telemetry

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

* fix(tools-pack): carry workspace build key into tarballs (#6235)

* fix(amr): recover late login failures and trace auth stages (#5986)

* fix(amr): recover late login failures and trace auth stages

* fix(amr): preserve scoped cancel attempt IDs

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

* fix(amr): defer unsupported Vela auth stages

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

* fix(amr): narrow fallback telemetry to shipped Vela

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

* fix(amr): preserve live login after stale cancel

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

* fix(amr): preserve cancel during login startup

Retain cancellation intent when a provisional cancel races the delayed canonical login response, then cancel the canonical attempt before polling can begin.

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

* fix(amr): rejoin newer login after startup cancel

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

* fix(amr): preserve cancel across web login starts

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

* fix(amr): retain cancel when status refresh fails

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

* fix(amr): cancel onboarding status preflight

Treat cancellation before an auth attempt exists as a local preflight cancel so a delayed status response cannot start login.\n\nGenerated-By: looper 0.11.1 (runner=fixer, agent=codex)

* ci: constrain Actions cache writes (#6270)

* fix(tools-pack): carry workspace build key into tarballs

* ci: constrain Actions cache writes

* fix(daemon): auto-mint assistantMessageId when clients omit pin (#6107)

* fix(daemon): auto-mint assistantMessageId when API clients omit it

Web always supplies assistantMessageId. Headless/MCP/eval clients that
already send conversationId but omit the pin left lastMessageId null and
skipped multi-turn native session resume (missing_cursor). Mint the id
server-side whenever a conversation is bound and the client did not, and
seed the user message the same way the old MCP-only path did.

* fix(daemon): validate conversation ownership before omit-pin seed

Reorder project-ownership checks before minting assistantMessageId and
seeding the user prompt so cross-project omit-pin requests cannot corrupt
foreign chats. Make ChatRunCreateRequest.assistantMessageId optional and
expose conversationId/assistantMessageId on McpRunCreateRequest with
contract tests for the omit-pin shape.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): reject missing conversation before omit-pin mint

Return CONVERSATION_NOT_FOUND when a client supplies a stale or mistyped
conversationId so omit-pin does not mint an unpersisted assistantMessageId
and plugin snapshot resolve does not FK-crash with a 500.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): seed latest turn and fallback-bound user prompts

Prefer currentPrompt over full message transcripts when seeding omit-pin
user turns, and still seed after conversationId fallback when the client
already supplied assistantMessageId.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): preserve attachments on omit-pin seeded user turns

Copy request attachments and commentAttachments into the server-seeded
user message so omit-pin/headless clients keep chips and annotation
context after conversation reload.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): treat empty currentPrompt as authoritative omit-pin seed

Attachments-only ChatRequest turns send currentPrompt as '' while message
is the flattened transcript. Prefer any string currentPrompt, including
empty, so seeded user rows keep empty content and do not duplicate history.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): seed turn metadata and bump project activity on omit-pin

Persist sessionMode, runContext, and appliedPluginSnapshot on omit-pin
seeded user turns so reload/retry keep applied context. Call updateProject
after seed so listProjects reorders headless/API activity like PUT /messages.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): require projectId for bound conversation ownership

Reject POST /api/runs when conversationId is present but projectId is
missing or non-string, so omit-pin cannot seed messages without owning
project context. Cover both early and post-fallback ownership checks.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): preserve slideIndex and BMP kind on omit-pin seeds

Keep deck annotation slideIndex and classify .bmp as image when seeding
user messages for clients that omit assistantMessageId.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): seed empty message when omit-pin has attachments

When currentPrompt is unset and message is empty, still seed the user
turn if attachment metadata is present so chips/annotations survive reload.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): seed omit-pin chat content from original request

Plugin resolution may rewrite meta.message with a rendered scenario
brief for the run; persist requestBody currentPrompt/message for the
user turn so the internal brief is not shown as chat content.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(web): persist browser preview viewport (#4899)

* fix(daemon): honor Kiro ACP turn completion (#6268)

* fix(daemon): isolate plugin-started Local Codex runs (#6273)

Disable Codex plugins only for externally attributed Open Design Plugin runs so Local Codex cannot recursively enter collect_brief or Cloud login. Preserve normal Local Codex plugin loading and the existing operator-wide override.

Validated with the focused runtime args suite, workspace typecheck, guard, and an isolated source smoke that produced a valid artifact without Cloud login.

Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>

* fix(landing): accept signed Vela team plan leads (#6283)

* fix: durably deliver enterprise leads

* fix(landing): make contact sales KV canonical

* feat(prompts): introduce on-demand discovery in SP v2.0 (#6223)

* feat(prompts): introduce on-demand discovery in SP v2.0

* fix(prompts): prevent nested deck label overlap

* fix(plugins): stabilize od-default task routing

* fix: address prompt and deck audit regressions

* fix(prompts): restore plan mode precedence

* fix(daemon): accept all form answer headers

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>

* fix(web): surface BYOK migration validation errors (#6280)

* fix(web): surface BYOK migration validation errors

Preserve structured daemon errors during legacy credential migration and reserve offline guidance for fetch failures.

Keep legacy browser credentials until migration succeeds, and cover HTTP validation, malformed responses, and network failures.

Validated with focused web tests, web typecheck, workspace guard, and workspace typecheck.

* fix(web): stabilize BYOK persistence error telemetry

Map secure-profile HTTP and network failures to semantic error codes before emitting settings_byok_test_result, while preserving existing fallback behavior for unrelated connection-test errors.

Validated with the complete web test suite, web typecheck, workspace typecheck, guard, and git diff --check.

---------

Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>

* fix(ui): improve dark-mode contrast for off-state toggles (#5825)

* fix(ui): improve dark-mode contrast for off-state toggles

The default off-state track uses --border-strong (#c9d0da light / #46433c dark).
In dark mode #46433c is only ~5% lighter than the --bg-subtle (#252321) panel
behind it, making the toggle nearly invisible when off and users unsure of the
control state.

Bump the off-state track to #6b6862 with a subtle inset border, and add a
thin outline to the white thumb so it stays visible against the (now lighter)
track. On-state already uses --text (#e8e4dc) and is left unchanged.

Applies to both .toggle-row (used by speaker-notes toggle in the deck tab)
and .compact-toggle (used by SurfaceOptions toggles). Hover lift gives an
additional interactive affordance.

Fixes #5298

* fix(ui): fix two dark-mode toggle blocking issues from review

Two correctness issues called out by mrcfps:

1. System dark theme never gets the contrast fix — the default "system"
   theme mode removes [data-theme] and falls back to
   @media prefers-color-scheme. Added a parallel block scoped to
   html:not([data-theme]) inside @media (prefers-color-scheme: dark).

2. Hover override wins over on-state — .compact-toggle:hover and
   .toggle-row:hover have higher specificity than .on
   variants, so hovering an ON toggle dropped its track from bright
   var(--text) to #75726c. Changed to :not(.on):hover so the hover
   lift only applies when off, leaving the on-state always bright.

Both applied to both selector paths (explicit data-theme and system dark).

* fix(web): keep Azure deployment names editable (#6034)

* chore(plugin-previews): refresh baked preview manifest (#6293)

Co-authored-by: open-design-bot <bot@open-design.ai>

* fix(byok): withdraw Windows DPAPI backend (#6308)

* fix(byok): withdraw Windows DPAPI backend

* fix(byok): clean up retired Windows secret blobs

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

---------

Co-authored-by: Looper <looper@noreply.github.com>

* fix(web): dismiss stale todo after continuation starts (#6307)

* fix(web): preserve shared project update timestamps

* fix(web): reconcile shared timestamps on home

* fix(web): preserve workspace scope in preview assets

* fix(collab): avoid unscoped shared cover requests

* fix(web): avoid double-wrapping preview assets

* fix(web): preserve first prompt across project refresh

* test(web): target the active shared project card

* test(web): keep empty BYOK profile type-safe

* test(e2e): require explicit workspace scope

* fix(web): finish scoped preview hydration

* test(e2e): align manual edit with current inspector

* fix(web): reconcile files after event stream readiness

* fix(web): keep local project creation workspace-independent

* fix(web): preserve scoped auth recovery state

* test(e2e): assert explicit Personal workspace scope

* test(e2e): preserve visual AMR account fixture

* test(web): await scoped cover source

* UI polish batch: cloud sign-in, onboarding, composer menu (#6281)

* UI polish batch: cloud sign-in, onboarding, composer menu

Sign-in surfaces (rail callout, balance-gate dialog, onboarding) had
drifted from the visual system and lost some redundant chrome; this
batch tightens them up alongside a couple of composer/design-files
reorganizations that were sitting in the same working tree:

- Rail's signed-out callout: reorder copy above the login pill, give
  the pill an icon + accent-green label, restore the Escape hatch
  the pill had before it lost its close affordance.
- AmrBalanceDialog (insufficient-balance / signed-out gate): add a
  full-bleed art banner above the copy, drop the now-redundant icon
  badge, tighten spacing/shadows to match.
- Onboarding cloud landing: switch from a single centered column to
  a two-column layout (form pane + full-bleed art panel), move the
  language switcher into the footer, swap the local-CLI/BYOK links
  for icon Buttons, and add a dismiss control to the activation-retry
  hint.
- Composer "+" menu: fold 插件/设计百宝箱 back in as hover-expand
  submenu rows instead of standalone quick pills above the input.
- Design files panel: move new-document/upload actions out of the
  always-visible toolbar into the empty-state actions only.
- Message center: drop the redundant header close button (backdrop
  click and Escape still dismiss it) and restyle the list from
  bordered cards to a divider-separated list.
- New `key` / `robot` / `translate` / `arrow-right` icons; language
  menu switches from the `languages` glyph to `translate`.
- Tab launcher popover reverts to the glass material; language
  dropdown gets a max-height + scroll so long lists don't blow out
  the popover.

Rebased the composer-quick-pill removal onto this branch's own
accessibility work on those pills (opener-based focus return via
`ComposerStandalonePanel`) rather than reverting it — the "next step"
card still drives the same standalone-panel API, just without a pill
to hand focus back to. Also updated/removed the tests that pinned the
now-removed surfaces (quick-pill popup contract, message-center close
button, onboarding top bar, design-files toolbar upload trigger) so
none of this lands with a newly-red suite.

* perf(web): optimize onboarding cloud artwork

* fix(web): invalidate project file reads after mutations

* fix(web): disable readonly project starter mutations

* fix(web): preserve focus for standalone composer panels

* test(e2e): require explicit workspace scope

* fix(web): invalidate file reads on project events

* test(e2e): seed explicit settings workspace

* test(e2e): align manual edit with current inspector

* fix(web): refresh deployments when share opens

* test(e2e): follow composer design system entry

* fix(web): keep local project creation workspace-independent

* fix(web): preserve scoped auth recovery state

* test(e2e): follow current entry settings flows

* fix(web): preserve AMR retry across settings

* test(e2e): model explicit personal workspace recovery

* test(e2e): stabilize AMR logout settings recovery

* fix(projects): return workspace scope on create

---------

Co-authored-by: lefarcen <935902669@qq.com>

* test(e2e): lock design system team partition

* fix(workspace): stabilize scoped project lifecycle

* test(e2e): stabilize retained workspace transitions

* fix(workspace): retain exact scope through transient outages

* test(workspace): lock outage identity boundaries

---------

Co-authored-by: Ray Xi <2667192167@qq.com>
Co-authored-by: PerishFire <39043006+PerishCode@users.noreply.github.com>
Co-authored-by: Looper <looper@noreply.github.com>
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
Co-authored-by: bestthanapon <thanapon.suwannasuk@gmail.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: Amy <58060647+AmyShang-alt@users.noreply.github.com>
Co-authored-by: Marc Chan <mrc@powerformer.com>
Co-authored-by: _HDCoder <2728073932@qq.com>
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: Nicholas-Xiong <2482929840@qq.com>
Co-authored-by: mehmet turac <mehmetturac@gmail.com>
Co-authored-by: open-design-release-bot[bot] <295937643+open-design-release-bot[bot]@users.noreply.github.com>
Co-authored-by: open-design-bot <bot@open-design.ai>
Co-authored-by: CHENGLONG WANG <honam884844@gmail.com>

* fix(workspace): seed dashboard workspace input

* test(workspace): cover live dashboard workspace input

* fix(workspace): backfill legacy workspace names

* test(e2e): harden workspace collaboration coverage (#6312)

Harden Workspace collaboration authority, lifecycle cleanup, and browser coverage.\n\nRetry an expired authorized Team-project pull exactly once with a fresh receipt while preserving fail-closed scope, version, cleanup, and final freshness checks. Align the fake Vela receipt lifetime with the real two-second contract and use scalable E2E timeouts.

* fix(workspace): refresh dashboard input on switch

* fix(viewer): avoid inert attribute warning

* fix(chat): preserve user turn ordering

* fix(workspace): stop retracted Team plugins immediately (#6335)

Prevent stale Team-plugin listings, materializations, and delayed positive hub reads from reactivating a resource after retraction. Preserve independent Personal plugins with the same manifest ID and keep Team members able to share their own resources.

Add deterministic regression coverage for cache invalidation, binding lifecycle, and the superseded-positive activation race.

* fix(workspace): harden Vela billing proxy boundaries (#6348)

Reject invalid wallet balances without overwriting valid cache state, validate normalized proxy paths and Workspace scope, strip hop-by-hop headers, and tear down upstream requests on disconnect.

Keep team Workspace scope fail-closed when Connection nominates the protected header.

* fix(web): tolerate unnamed workspace directory rows (#6349)

Preserve a verified explicit Workspace context when legacy directory rows omit the display-only workspaceName field. Keep exact-row name backfill when a non-empty name is present, without inferring authority from active or default Workspace state.

* fix(workspace): enforce exact project-list scope

* fix(workspace): show team design systems in pickers

* fix(web): reserve image cards while thumbnails load

* fix(web): keep home creation types ready during refresh

* test(e2e): align Windows beta smoke with installer

* perf(web): start team catalog from exact directory identity

* fix(web): refresh revoked workspace context

* fix(web): order collaboration presence refreshes

* fix(amr): prefer selected Vela OpenCode companion

* fix(web): isolate collaboration presence sessions

* fix(web): parallelize scoped project route bootstrap

* perf(daemon): cache workspace project list authority

* fix(web): preserve project-open connection budget

* test(web): preserve memory toast exports in app mock

* fix(web): preserve exact project opening scope

* fix(daemon): coalesce scoped comment pulls

* fix(daemon): retry team comment relay delivery

* fix(daemon): atomically queue comment relay writes

* fix(collab): show team comments across local conversations

* fix(daemon): preserve catalog-only projects on unshare

* fix(collab): close comment realtime catch-up gaps

* docs: add workspace team rollout runbook

* fix(collab): retry failed Team project shares

* fix(workspace): isolate personal projects in recent lists

* fix(daemon): isolate personal design systems

* fix(workspace): authorize runs before plugin snapshots

* test(daemon): align workspace retry contracts

* fix: isolate plugins and skills by workspace member

* fix(workspace): scope design systems in run prompts

* fix(workspace): gate runs before plugin resolution

* fix(daemon): namespace team design system bindings

* fix: close plugin and skill scope leaks

* test(collab): cover project-scoped comment actions

* fix(workspace): scope project resource selection

* fix(workspace): preserve personal design system reads

* fix(workspace): authorize scoped run resources first

* fix(workspace): keep project design systems scoped

* fix(types): carry design system member scope

* fix(billing): scope personal balances by workspace

* fix(workspace): preserve unbound local design systems

* fix: resolve team design systems by canonical binding

* fix(workspace): invalidate nested project catalog cache

* fix: keep team design system runs canonical

* perf: batch durable comment relay delivery

* fix: cancel stale local comment relays

* fix(web): preserve scoped project display snapshots

* fix(web): synchronize project rename projections

* perf: reuse request-scoped workspace authority

* test: enforce personal project creator ownership

* fix(web): wait for project workspace authority

* fix(daemon): invalidate team project caches before signaling

* fix(web): order targeted project metadata refreshes

* fix(workspace): preserve team catalog on outages

* fix(web): isolate catalog display across accounts

* fix(web): scope extension marketplace catalogs

* fix(web): order broad team catalog refreshes

* fix(web): scope home plugins to workspace identity

* fix(web): isolate plugin views by workspace identity

* fix(web): polish sign-in and composer focus

* fix(web): preserve scoped project state after team moves

* fix(workspace): enforce scoped design and billing identity

* test(e2e): align workspace flows with scoped bootstrap

* fix(collab): drain newer comment revisions promptly

* test(e2e): keep owner actions after workspace lock

* fix(web): converge project renames across scoped lists

* fix(web): preserve pending projects across view changes

* fix(web): restore preview runtime after activation

* test(e2e): align workspace scope assertions

* test(web): cover hidden stream recovery

* fix(web): update deep-link project titles after rename

* fix(collab): retry project metadata propagation

* fix(web): preserve legacy design system logos

* fix(web): wait for activated preview bridge

* fix(design-systems): isolate team resource storage

* fix(plugins): reject team mirror mutations

* fix(web): partition extension catalogs by workspace

* fix(skills): isolate team mirrors by workspace

* fix(plugins): keep local share projects unscoped

* fix(web): retry preview state capture during activation

* test(e2e): scope project workspace fixtures explicitly

* feat(collab): coordinate team resource invalidations

* fix(collab): emit team resource changes after sync

* fix(web): invalidate project lists after updates

* fix(web): serialize project renames by scope

* fix(collab): preserve team project comment anchors

* fix(web): refresh shared resources after remote changes

* fix(collab): materialize team projects before rename

* fix(web): keep workspace catalogs current

* fix(web): close catalog and credential races

* test(workspace): harden team workflows and packaged deep links (#6358)

* test(workspace): harden team workflows and deep links

* fix(packaged): deliver hot invite deeplinks

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

* fix(workspace): catch up team resources safely

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

* fix(workspace): invalidate resource catch-up changes

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

* fix(workspace): catch up shared resource changes

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

* fix(packaged): preserve fallback invite deeplinks

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

* fix(workspace): hide retracted team skills

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

* test(e2e): align workspace CI fixtures with route bootstrap

* fix(collab): complete design system reconnect adoption

* feat(analytics): track Workspace interactions (#6397)

* feat(analytics): track Workspace interactions

* fix(analytics): dedupe community page views

---------

Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>

* fix(daemon): preserve team comments across conversations

* fix(collab): keep presence rosters converged

* fix(comments): preserve routed comment anchors

* fix(workspace): reconcile team project renames

* fix(collab): preserve member directory last-good state

* perf(collab): start scoped Team presence before status

* fix(comments): keep relay anchors internal

* perf(collab): warm presence from daemon cache

* perf(collab): cancel obsolete cold status polls

* fix: fence presence sessions against late heartbeats

* perf(collab): share team member roster scheduling

* fix(collab): preserve roster store through strict replay

* perf(collab): retain workspace member directory stores

* fix(collab): resolve presence identities from team directory

* fix(collab): hide unresolved presence identities

* fix(collab): prefer directory presence names

* perf(web): preload workspace member directory

* test(collab): preserve no-leave lifecycle contract

* fix(web): preserve personal workspace presence after share

* fix(web): refresh covers after team materialization

* fix(web): refresh files after team materialization

* fix(web): gate shared file reads on project authority

* fix(web): preserve workspace folder action sizing

* ci(e2e): split project-workspace UI P0 shard into project-workspace + project-collab

project-workspace pins workers=1 by design (deliberate, not incidental —
see e2e/AGENTS.md's UI test stability rules on cross-file worker carry-over),
so its 7 files ran fully serially: ~26min wall time in recent merge-queue
runs, the longest pole in the ui_p0 matrix by a wide margin over its
10-13min siblings.

workspace-multi-client-collab.test.ts alone (spins up two isolated
client/daemon runtimes per case) accounted for ~10 of those minutes.
Carving it and workspace-keyboard-flows.test.ts into a new project-collab
shard — still workers=1, still one file at a time — lets the two halves
run as separate concurrent CI jobs instead of one long serial chain,
without touching the single-worker-per-shard isolation the pin exists for.

Updates the topology guards that pin the exact matrix shape so the split
doesn't drift silently: DAEMON_RUNTIME_DEFINITION_MATRIX_NAMES (scopes.ts),
the full/candidate matrix name guards (guard/scope.ts), and the fixed
expectations in scopes.test.ts / packaged-smoke-workflow.test.ts.

Validated: pnpm guard (UI P0 shadow contract + CI topology checks pass),
pnpm typecheck (all workspaces), and the two topology test files
(107/107 passing).

* fix(collab): revoke unshared team project mirrors

* fix(web): preserve unbound project authority across workspace changes

* fix(web): pair scoped raw queries with headers

* test(e2e): pin writable scope for project actions

* test(web): cover Personal authority recovery after reload

* test(web): await version preview readiness

* fix(workspace): scope brand design system resources

* fix(workspace): materialize owner team design systems

* fix(web): fence design system reads by generation

* fix(web): propagate brand read generations

* fix(web): preserve workspace project visibility

* fix(preview): retain scope for runtime relative assets

* fix(web): explain design system delete denial

* fix(daemon): bound team resource prewarm scopes

* fix(daemon): recheck prewarm lease after queue

* fix(web): stabilize transient presence roster gaps

* chore(pack): pin Vela CLI 0.0.28

* fix(workspace): move design system project with team share

* fix(workspace): gate linked unshare on live authority

* fix(workspace): ensure backing project before design share

* fix(web): scope srcdoc preview assets

* perf(web): mint preview scope only for srcdoc

* perf(web): reuse srcdoc preview scope

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

pnpm-lock.yaml moved @powerformer/vela-cli from 0.0.27-test.7 to 0.0.28, but nix/pnpm-deps.nix was not regenerated, so `nix flake check` failed with a fixed-output hash mismatch for both the daemon and web derivations.

Hashes taken from the Nix-reported expected values in CI run 30933223422 (nix is not available on this machine to run `pnpm nix:update-hash` locally); the merge of origin/main did not touch pnpm-lock.yaml, so those values still describe this tree.

* chore(ci): retrigger workspace-team validation

The previous ci run for this branch sat pending with zero dispatched jobs; cancelling it and pushing an empty commit re-enters the ci-6142 concurrency group from a clean slot.

* perf(web): skip preview mint for authored base

* fix(collab): preserve presence through lease gaps

---------

Co-authored-by: Ray Xi <2667192167@qq.com>
Co-authored-by: CHENGLONG WANG <honam884844@gmail.com>
Co-authored-by: lefarcen <ontf116@gmail.com>
Co-authored-by: PerishFire <39043006+PerishCode@users.noreply.github.com>
Co-authored-by: Looper <looper@noreply.github.com>
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
Co-authored-by: bestthanapon <thanapon.suwannasuk@gmail.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: Amy <58060647+AmyShang-alt@users.noreply.github.com>
Co-authored-by: Marc Chan <mrc@powerformer.com>
Co-authored-by: _HDCoder <2728073932@qq.com>
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: Nicholas-Xiong <2482929840@qq.com>
Co-authored-by: mehmet turac <mehmetturac@gmail.com>
Co-authored-by: open-design-release-bot[bot] <295937643+open-design-release-bot[bot]@users.noreply.github.com>
Co-authored-by: open-design-bot <bot@open-design.ai>
Co-authored-by: shangxinyu1 <shangxinyu@refly.ai>
Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
2026-08-05 08:31:54 +08:00
Tom Huang b99a9fdc3d feat(manual-edit): add direct manipulation and reliable history (#5890)
* feat(web): manual edit v2 direct manipulation (v2.2–v2.4)

Selection overlay with action bar and drag/resize handles, floating text
toolbar with range-format highlight (queryCommandState round trip), Manus
color picker, image replace/crop/paste/drop, element copy/duplicate/delete,
and the systematized in-place content pipeline: every element-scoped patch
now reconciles the live iframe DOM from the saved source over
od-edit-apply-dom (replace/insert-after/append-child/prepend-child/remove)
with positional identity restamping, so commits, undo and redo never reload
the canvas, never flash, and never move the scroll position. Frozen-source
reloads remain only as a scroll-capturing fallback. Includes the Chromium
Home/End double-booking fix, the dblclick word-selection guard, and light
targets-broadcast dedupe.

* test(web): manual edit v2 unit and e2e coverage

Bridge (in-place apply ops, positional restamp, selection format state,
Home/End consumption, dblclick selection guard), source-patches (insert
readback, restore descriptors, sanitization), gestures/shortcuts/color
units, and host-level FileViewer suites proving content commits mutate the
canvas without a srcDoc reload, hand selection to inserted elements, and
record labeled manual versions for undo/redo. Also realigns the
manual-edit-history and srcdoc-reload-races helpers with the v2.1 opt-in
inspector two-step flow.

* docs(specs): manual edit v2 architecture spec

Six-layer map (identity/chrome/style/content/history/external-sync), core
invariants (DOM equals saved source without reloads; positional identity
restamps on structural change; every save is a labeled manual version),
patch-kind application matrix, performance contract, and the v2.1–v2.4
revision log including the real-browser verification round.

* fix(tools-dev): fail fast when a sidecar process dies during startup wait

waitForDaemonRuntime/waitForWebRuntime now accept a liveness probe for the
spawned pid, so a crashed daemon/web process surfaces immediately instead
of burning the full startup timeout.

* fix(web): edit brand-kit pages in place — no reload flash, no scroll jump

Brand-kit targets carry runtime-annotated ids with no markup of their own in
the saved source (edits persist into the payload / runtime overrides), so
the in-place readback came up empty and every commit fell back to a
srcDoc reload — the flash-and-jump-to-top the user hit when editing text
after an image. New od-edit-apply-dom op 'apply-content' mirrors the patch
fields (text/href/src/alt/html/attributes, on*/data-od-* filtered) onto the
live element instead, and set-outer-html reuses replace with the patch
html. The reload fallback's scroll restore also gains a clamped-only retry
ladder (600–4000ms) so async-rendering pages (brand kit renders after
load) no longer land at the top when a reload is genuinely needed.

* refactor(FileViewer): unify manual edit history management and enhance undo/redo functionality

This update introduces a consistent approach to managing manual edit history and undo/redo stacks by utilizing refs to mirror state. The new `updateManualEditHistoryStack` and `updateManualEditUndoneStack` functions ensure that the state remains in sync with the refs, preventing stale data issues during undo/redo operations. Additionally, the handling of in-session undo (Cmd+Z) has been improved to escalate to the host's global history when no local changes remain, ensuring a seamless user experience. Tests have been added to verify the correct behavior of these features.

* feat(manual-edit): enhance manual edit functionality with space scaling and improved gesture handling

This update introduces a new `ManualEditSpaceScale` interface to manage the scaling between viewport pixels and CSS pixels, ensuring accurate positioning during manual edits. The `manualEditMovePreviewTransform` and `manualEditResizeStyles` functions have been updated to utilize this scaling, improving the precision of drag and resize gestures. Additionally, the `ManualEditSelectionOverlay` component now maintains pointer capture during gestures to prevent interaction issues with nested iframes. Tests have been added to verify the correct behavior of these enhancements, ensuring a smoother user experience in manual editing scenarios.

* feat(FileViewer): add localized toasts for image upload process in manual edit

This update enhances the manual editing experience by introducing localized toast notifications for the image upload process. New messages for uploading, processing, and successful addition of images have been added to the i18n files. The `insertManualEditImage` function now manages these notifications, providing users with real-time feedback during image uploads. Additionally, tests have been implemented to ensure the correct display of these toasts across different locales.

* fix(manual-edit): drag whole image to move + lock selection frame to element box (#5888)

Two direct-manipulation fixes reported from real prototype/landing/deck edits:

1. Move by dragging the image body, not only the top pill. Adds a
   full-interior moveBody surface inside the selection frame that starts the
   same undoable move gesture; scoped to image targets so text/link bodies
   keep click-to-edit. Rendered under the resize handles so edges still resize.

2. Keep the selection frame locked to the element during resize. The frame
   previously drew the gesture-intent x/width and only adopted the iframe's
   measured y/height, so an element that re-centers, caps at max-width, or
   reflows on width change left the frame drifting horizontally off the
   picture. adoptMeasuredExtent now takes the full measured rect and
   displayRect follows it, so the chrome always wraps the element.

Adds ManualEditSelectionOverlay tests for body-drag move and measured-box
frame sync; records the change as spec v2.5.

Co-authored-by: Claude <noreply@anthropic.com>

* test(web): stabilize manual edit browser validation

* fix(manual-edit): address structural edit review blockers

* fix(manual-edit): preserve runtime formatting and restore order

* fix(manual-edit): flush toolbar styles before navigation

* fix(manual-edit): preserve runtime target identity

* fix(manual-edit): serialize saves and image handoff

* fix(manual-edit): clear stale runtime clipboard

* fix(manual-edit): scrub rich text identities

* fix(manual-edit): save inspector text after formatting

* fix(manual-edit): action-bar boundary avoidance, select-without-reflow, image paste shortcut (#5892)

Three edit-mode canvas fixes from user feedback:

1. Action bar no longer covers the element frame/handles. A small or
   edge-hugging element previously had its move pill and resize handles
   hidden under the floating action bar (clamped to the top), so it could not
   be moved or resized. New pure helper manualEditActionBarTop places the bar
   above the frame, flips it below when the element hugs the top edge, and
   keeps it inside the canvas; horizontal clamping already handles the side
   edges. Adds a data-placement attribute.

2. Selecting a text element no longer makes it jump/grow. The bridge fused
   'select' and 'enter inline edit' into one click, and making a deck text box
   contenteditable releases its line-clamp/height cap so it visibly reflows
   larger the instant it is selected. The first click now only selects (no
   contenteditable, no reflow); editing begins on a second click of the
   already-selected element or a double-click — matching how images/containers
   already select.

3. A selected image/container can be pasted with Cmd/Ctrl+V. The native paste
   event only fires while an editable element holds the caret, which never
   happens for non-text selections, so the shortcut did nothing for images.
   The bridge keydown handler now forwards od-edit-paste-request for a selected
   non-editable element (paired with the existing Cmd/Ctrl+C copy).

Updates the 14 inline-edit bridge tests to enter edit via double-click and
adds coverage for all three fixes; recorded as spec v2.6.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(manual-edit): image corner scaling, nowrap unlock on resize, restore image paste

Three edit-mode canvas fixes from user feedback:

1. Images get four corner anchors that scale aspect-locked from any corner,
   anchored at the opposite corner (new resize-nw/ne/sw/se gesture kinds).
   The dominant pointer axis drives the scale, the floor binds the larger
   dimension, and corner gestures skip snapping so the ratio never breaks.
   Corner resizes persist width AND height (space-scale corrected) plus the
   offsets that keep the anchored corner fixed.

2. Resizing an element with white-space: nowrap (or pre) now unlocks
   wrapping (normal / pre-wrap) alongside the width — a narrowed heading
   previously stayed on one line and overflowed its own new box.
   ManualEditStyles and the bridge style props gain whiteSpace.

3. Image paste works again. The v2.6 Cmd/Ctrl+V keydown branch called
   preventDefault, which suppresses the native paste event — the only place
   clipboard image bytes are readable — so pasting an image did nothing.
   The shortcut now arms a 150ms one-shot element-paste fallback instead;
   any real paste event disarms it and routes image vs element itself, so
   the two channels never double-fire.

Adds gesture/overlay/bridge coverage for all three; recorded as spec v2.7.

* fix(manual-edit): replay runtime history in place

* fix(brands): reject scraped CSS junk font families before token emission

An extraction can capture CSS source text instead of a real font family
(e.g. Tailwind v4's `--theme(--default-font-family` from aliyun.com).
Interpolated raw into the kit's `--brand-font-family`, the unbalanced
parenthesis swallows every later :root declaration, so the component kit
renders unstyled (UA serif, collapsed buttons) while earlier color tokens
still work.

isRenderableFontFamily() now guards both emission seams: the seed's
fontStack (kit/variables/artifacts token block) and brandFontAssets'
display-family stack (h1-h3 face). Junk families drop out and the
renderable fallbacks + system stack take over.

* fix(manual-edit): replay runtime-only style history from the persisted override rule

Runtime-only brand-kit targets persist set-style patches in the
style[data-od-manual-edit-runtime-overrides] rule, not as element inline
styles. The in-place history replay read inline styles only, so redo of a
runtime style change replayed empty values while reporting success — the
file held the redone style while the canvas stayed on the undone one.

readManualEditSavedStyles() now reads a target's persisted styles wherever
they live (inline attribute for source-backed nodes, the runtime override
rule otherwise), and the set-style history branch replays through it. The
same inline-only read also made reconcileManualEditStyleSave() treat every
runtime style save as "target vanished" — erroring, dropping the selection,
and reloading the canvas — so it now consults
hasManualEditRuntimeStyleOverride() before taking that path.

Regressions: source-patches unit coverage for the saved-styles read and the
override predicate, plus a FileViewer spec that styles a runtime target,
undoes and redoes it without any srcdoc change, and asserts the redo
preview carries the persisted value (red on the previous head).

* fix(manual-edit): merge partial runtime style patches into the persisted rule

Successive debounced toolbar autosaves send only the touched property, but
setRuntimeStyleOverride() rebuilt the target's override rule solely from the
incoming patch, so a runtime-only brand-kit target lost its earlier saved
declarations (fontSize save, then color save -> font-size silently dropped).
Persistence now merges the incoming patch into the existing rule
declarations, treating empty incoming values as deletions and removing the
rule once its last declaration is deleted.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 04:09:02 +00:00
open-design-crew[bot] d8b6b797fe Simplify chat execution disclosures (#5667)
* Simplify chat execution disclosures

* test: cover collapsed file disclosures

* refactor(web): consolidate chat task activity

* fix(web): show small file outputs inline

* fix(web): separate file outputs from reads

* fix(web): collapse completed todo cards

* refactor(web): compact completed todo lists

* fix(web): make completed todos expandable

* fix(web): keep four output files visible

* refactor(web): clarify chat activity timeline

* fix(web): make file output disclosure interactive

* feat(web): emphasize chat run states

* style(web): simplify chat run status colors

* style(web): remove chat run status icons

* feat(web): stream one chat activity at a time

* style(web): align chat completion row

* fix(web): simplify priority chat states

* feat(web): simplify secondary chat details

* style(web): simplify secondary chat hierarchy

* fix(web): keep execution state out of artifacts

* fix(web): use icons for artifact operations

* style(web): frame todo disclosure header

* fix(web): clarify artifact and tool semantics

* fix(web): clarify chat disclosure hierarchy

* fix(web): keep execution history neutral

* fix(web): simplify user action cards

* fix(web): align failure card actions

* fix(web): refine chat action hierarchy

* fix(web): address chat disclosure review feedback

* fix(web): restore chat recovery actions

* fix(web): make collapsed action details inert

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
2026-07-21 07:44:52 +00:00
lefarcen c9e622d0a9 fix(daemon): ship baseline OpenCode for Windows and stop retrying AVX2 illegal-instruction crashes (#5733)
* Classify AVX2 illegal-instruction agent crashes as cpu_unsupported and stop retrying them

The Windows opencode.exe bundled with the AMR runtime is a Bun build that
requires AVX2. On CPUs without it (Intel Atom/Celeron/Pentium N-series
through 2021) the binary dies with STATUS_ILLEGAL_INSTRUCTION before
readiness; vela surfaces an ACP fatal, the daemon classified it as a
retryable fatal_rpc_error, and the automatic retry deterministically
failed again. 88 of 105 pre-readiness Windows crash traces in Langfuse
are this single failure.

The crash text (Bun 'Illegal instruction' banner with a no_avx2 CPU
feature line, or the raw 0xC000001D / decimal 3221225501 exit status) now
classifies as process_exit/cpu_unsupported ahead of the fatal_rpc_error
close-reason promotion, is never auto-retried, and renders a dedicated
error card telling the user to update Open Design instead of a dead
Retry button.

* Bump @powerformer/vela-cli to 0.0.23 so packaged Windows builds bundle the baseline (non-AVX2) OpenCode

The 0.0.23 win32-x64 platform package compiles the OpenCode fork with
--target=windows-x64-baseline, so the bundled opencode.exe no longer
requires AVX2 and starts on Intel Atom/Celeron/Pentium N-series CPUs.
Verified in vela CI: the build log shows the baseline target and the new
post-publish smoke job boots the published bundle's opencode.exe on a
real Windows runner (powerformer/vela#838).

* Refresh Nix pnpm deps hashes for the vela-cli 0.0.23 lockfile change

* Add the abort-after-panic (exit status 3) production shape to cpu_unsupported specs

A user on an AVX-but-no-AVX2 CPU (Sandy/Ivy Bridge era) hit the same
illegal-instruction crash, but Bun panicked during its own panic handler
and abort()ed, so vela reported exit status 3 instead of
STATUS_ILLEGAL_INSTRUCTION. The stderr banner (no_avx2 / Illegal
instruction) is the only signal; encode the real Langfuse trace shape so
the classifier keeps catching it.

* Bump @powerformer/vela-cli to 0.0.25 so packaged Windows builds bundle the baseline (non-AVX2) OpenCode

0.0.25 is the first stable cut after powerformer/vela#838 merged: its
win32-x64 package compiles the OpenCode fork with
--target=windows-x64-baseline (verified in the release CI log), the
post-publish smoke boots the published bundle on a real Windows runner,
and the lockfile regenerator now pins the baseline asset so a routine
version bump cannot silently flip back. 0.0.24 predated that merge and
still shipped the AVX2 build.

* Tighten cpu_unsupported to precise signals only (no bare 'Illegal instruction')

Review is right that a bare illegal-instruction line is too broad: an
unrelated SIGILL on an AVX2-capable machine (runtime bug, corrupted
jump) would be mislabeled 'Processor not supported' and lose its retry.
Bun's crash banner always prints the CPU-feature line, so machines
actually missing AVX2 are identified by no_avx2; the bare Windows
STATUS_ILLEGAL_INSTRUCTION renderings (0xC000001D / 3221225501) stay
because those runs carry no banner at all. Generic illegal-instruction
banners without no_avx2 return to process_crashed / fatal_rpc_error,
locked by a new negative spec.

* Refresh Nix pnpm deps hashes for the vela-cli 0.0.25 lockfile change

* Gate raw STATUS_ILLEGAL_INSTRUCTION on the bundled-opencode startup context
2026-07-21 06:10:22 +00:00
mehmet turac ace06eac10 fix(web): respect preview viewport in image exports (#5828) 2026-07-21 03:10:38 +00:00
Ethan Guo 85ec1b6243 fix(web): sanitize deck thumbnail markup with DOMPurify (#5847) 2026-07-21 02:26:02 +00:00
lefarcen c8bad6bcb4 fix(web): keep report-only Design runs off the ARTIFACT_NOT_FOUND path (#5724)
A successful Design turn with zero produced files is only a missing
deliverable when the run actually attempted to mutate project files.
Image-analysis and report-only audit turns answer in prose without ever
calling a write tool; classify them as report_only and let them finish
as normal text results instead of appending ARTIFACT_NOT_FOUND.
2026-07-16 06:40:05 +00:00
Ethan Guo 05cb03c8ab fix(web): sandbox the speaker-notes presenter deck iframes (#5571) 2026-07-15 12:19:46 +00:00
open-design-crew[bot] 167db9de20 feat(web): add preview delivery status feedback (#5557)
* feat(web): add preview delivery status feedback

* fix(web): keep delivery retry in chat

* feat(web): stage preview delivery progress

* refine(web): clarify preview delivery stages

* fix(web): preserve preview delivery feedback

* fix(web): address preview delivery review

* refine(web): keep preview delivery status passive

* Revert "refine(web): keep preview delivery status passive"

This reverts commit df5579da48.

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
2026-07-15 09:47:02 +00:00
lefarcen b5d9a12f48 fix(web): break redirect-loop scripts that freeze the HTML preview (#5469)
* fix(web): break redirect-loop scripts that freeze the HTML preview

A generated or hand-edited artifact carrying a self-redirecting directive
— most reliably a `<meta http-equiv="refresh">` that reloads the same
document, or a cycle of meta refreshes (A -> B -> A -> ...) — reloads the
preview iframe forever and pegs the main thread until the whole design
workspace freezes. There was no loop detection or hop limit, so the
preview stayed unusable.

Add an in-iframe circuit breaker (`injectPreviewRedirectGuard`, always
injected by `buildSrcdoc`) that counts meta-refresh hops in `window.name`
— the one store that survives an iframe navigating itself — resets the
count once a full window elapses with no further refresh (timeout
safeguard so a slow legitimate auto-refresh never accumulates), and once
the hop budget is exceeded, or immediately for a near-instant
self-refresh, strips the offending `<meta>`, stops the frame, and posts
`od:redirect-loop-blocked` to the host. The host parks the srcDoc iframe
on a static "loop detected" placeholder, the reliable stop the browser's
unforgeable `window.location` otherwise denies.

Route any self-redirecting source onto the srcDoc path
(`htmlNeedsRedirectGuard`) so the guard is always present instead of the
URL-load path serving the document raw and unguarded.

Covered by a runtime spec that runs the real injected guard through a VM
modelling a sandboxed iframe reloading itself (hop budget, window reset,
immediate self-refresh kill, chain-break, host message) plus render-mode
detection specs.

Source: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/94709b20-f403-4ed7-a960-1e26572fa394

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): cover srcdoc redirect guard gaps

* fix(web): avoid redirect guard false positives
2026-07-15 07:07:34 +00:00
lefarcen 1278816a8f fix(web): route cross-project chat file links in-app instead of a home-page window (#5611)
* fix(web): route cross-project chat file links in-app instead of a home-page window

Chat file links pointing at @-referenced projects arrive as absolute
managed-projects disk paths or app file routes for another project.
asInProjectFilePath refused both, so the click fell through to the
default target="_blank" behavior and Electron's window-open handler
produced a detached child window whose unroutable path rendered the
home screen instead of the file.

resolveChatFileLink now classifies those hrefs and navigates to the
owning project's file route in the same window; any remaining
schemeless path-like href is swallowed instead of falling back to the
useless home window. Thinking-block markdown gets the same handler.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* worker: 当有文件链接在chatpane中出现时,点击文件链接唤起的是home页的预览

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): classify owning-project chat links before the basename fallback and keep the handler on opener-less surfaces

Review round on #5611:

- resolveChatFileLink now classifies hrefs that name their owning project
  (app file routes, managed-projects disk paths) BEFORE delegating to
  asInProjectFilePath, so a cross-project /…/projects/other/index.html can
  no longer be captured by a same-named file in the current project and
  silently open the wrong workspace tab. Legacy name-keyed directories
  (projects/<name>/ from 0.10.x previews) consequently navigate to that
  name instead of basename-matching — an in-window bounce at worst.

- chatFileLinkClickHandler is always installed; only the workspace-file
  open action is gated on onRequestOpenFile. Surfaces that mount the chat
  without a workspace opener (design-system chat in DesignSystemFlow)
  now still navigate cross-project targets and swallow unresolvable
  path-like hrefs instead of falling through to the detached home window.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* chore: drop the looper task attachment accidentally committed to the branch

.looper-attachments/ is runner-local reference material and must stay out
of git; a stray worker commit (d17c3cc) had checked the bug screenshot in.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): restore the current-project fallback for ambiguous disk paths and narrow href swallowing to file-like links

Review round 2 on #5611:

- resolveChatFileLink resolves the current project (including the
  known-file basename fallback for absolute paths) BEFORE managed-projects
  disk-route navigation. A /projects/<seg>/ boundary cannot positively
  prove another project's ownership client-side: legacy 0.10.x preview
  dirs are keyed by project NAME and imported folders may contain their
  own projects/ directory — both reference current-project files, per the
  maintained e2e contract (e2e/ui/project-file-link-routing.test.ts,
  verified locally: passes again). Only paths no current-project file
  matches navigate to the owning project (the actual 0.14.1 scenario).

- isPathLikeChatHref only swallows confirmed file-like hrefs (final
  segment carries an extension) and never protocol-relative URLs, so SPA
  routes like /automations or /projects/<id> and //host/… links keep
  their default behavior instead of becoming dead links.
  extractManagedProjectsDiskRoute also refuses //host/… hrefs.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): prove disk-link ownership from the project's resolvedDir instead of inferring from /projects/ boundaries

Review round 3 on #5611: an imported-folder workspace whose baseDir
itself contains a projects/ segment (e.g. /Users/x/workspace/projects/acme)
made resolveChatFileLink misroute just-written current-project files to a
phantom 'acme' project, because ownership was inferred from any
/projects/<seg>/ boundary once the known-file match failed.

Ownership is now decided only from positive proof against the daemon-
computed resolvedDir (GET /api/projects/:id), plumbed from ProjectView
through ChatPane into AssistantMessage's link handler:

- a href under resolvedDir is a current-project file even when absent
  from the (stale) file list — imported folders included;
- for managed projects resolvedDir reveals the projects root, so sibling
  paths provably belong to that other managed project and navigate there
  (the 0.14.1 @-reference scenario; proof also outranks the basename
  fallback on collisions);
- a /projects/<seg>/ boundary anywhere else proves nothing and never
  yields project-file — the generic extractManagedProjectsDiskRoute
  inference is removed; unproven absolute paths only resolve through the
  current-project known-file fallback (e2e contract re-verified locally).

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): thread projectResolvedDir into the side-chat and design-system ChatPane hosts

Review round 4 on #5611: the resolvedDir proof anchor only reached the
primary ProjectView ChatPane. The side-chat tab (FileWorkspace ->
SideChatTab) mounted ChatPane without projectResolvedDir OR
projectFileNames, and DesignSystemFlow without projectResolvedDir, so
absolute managed disk links stayed unclassifiable (inert) on those
production chat surfaces.

- SideChatTab accepts and forwards projectResolvedDir; FileWorkspace
  passes its existing resolvedDir prop plus a known-file set derived
  from the same files list ProjectView feeds its primary ChatPane.
- DesignSystemFlow fetches the workspace project's resolvedDir via
  getProjectDetail and threads it into its ChatPane.
- Host-level regression added above AssistantMessage: FileWorkspace ->
  SideChatTab -> real ChatPane -> real AssistantMessage, asserting a
  managed cross-project disk link navigates in-window and a stale
  current-project disk link opens as a workspace tab. SideChatTab
  prop-forwarding spec added alongside.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): swallow unroutable chat hrefs by asking the router instead of an extension heuristic

Review round 5 on #5611: narrowing isPathLikeChatHref to extensioned
last segments reopened the detached home window for extensionless file
links (Dockerfile, /tmp/README, ../Makefile, bare binaries) — their
clicks fell back to target=_blank again.

Routability is now decided by parseRoute itself: its catch-all home
fallback IS the detached-home-window symptom, so exactly the schemeless
hrefs that would trigger it are swallowed. Recognized SPA routes
(/automations, /projects/<id>, /design-systems/<id>, ...) keep their
default behavior — including gated routes, since the router's own flag
handling decides. Relative hrefs reaching the check are always
unresolvable (traversals, malformed encodings) and stay inert.

Regressions added for Dockerfile/README/Makefile shapes at the helper
and click-handler level; the round-4 spec that locked in false for
/usr/local/bin/node is flipped to the corrected expectation.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): harden the routability check against throwing parseRoute and the root-path fallback shape

Review round 6 on #5611: parseRoute() decodes route segments with a bare
decodeURIComponent, so a malformed same-origin href like /projects/%E0
made isPathLikeChatHref throw out of the click handler; and the root
path (/, /?q=1) shares the {kind:'home', view:'home'} shape with the
unknown-route catch-all, so a legitimate link to the app root was
swallowed.

isPathLikeChatHref now special-cases the root path as routable before
consulting the router and wraps parseRoute in try/catch — an href the
router would crash on is not a route and stays inert. Regression specs
cover malformed app-route shapes (helper + click level, asserting no
uncaught error) and the root path.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): classify Windows drive-letter hrefs as disk paths instead of URI schemes

Review round 7 on #5611: C:\... and C:/... hrefs match the RFC 3986
single-letter-scheme grammar, so isPathLikeChatHref treated them as
external URLs and Windows disk links skipped the inert-link swallow
entirely, reopening the detached home window on that platform; the
resolvedDir proof also only compared POSIX shapes.

- isWindowsDrivePath classifies drive-letter paths BEFORE the generic
  scheme check: an unresolved one is always inert (the SPA router can
  never serve it).
- resolveAgainstResolvedDir now compares over normalizeDiskPath output
  (backslashes unified to '/', drive letter case-folded), so the
  current-project prefix proof and managed-root sibling navigation work
  against a Windows daemon's resolvedDir, including mixed-separator
  hrefs; the traversal guard runs on the normalized form.

Regressions cover Dockerfile-style drive-letter hrefs at the helper and
click level plus resolver proof cases for backslash and mixed-separator
shapes.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): case-fold Windows disk-path proofs while preserving returned casing

Review round 8 on #5611: normalizeDiskPath only case-folded the drive
letter, so a valid mixed-case pair like C:\users\ME\...\project-1 vs a
resolvedDir of C:\Users\me\...\project-1 — the same location on a
case-insensitive filesystem — failed both prefix proofs and the click
was swallowed instead of opening the file or navigating.

resolveAgainstResolvedDir now runs its prefix/suffix proofs over
case-folded comparison keys for Windows drive-letter paths (POSIX stays
exact — Linux filesystems distinguish case), while projectId/filePath
are still sliced from the original normalized strings so their casing
survives into the returned target. Folding falls back to exact
comparison when lower-casing would change string length, keeping slice
offsets aligned for rare locale characters.

Coverage: mixed-case current-project and sibling-project proofs (red on
the previous head), original-casing preservation for the navigated
projectId, and a POSIX case-sensitivity guard.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): open design-system chat file links via the Files tab and prove UNC-share ownership

Review round 9 on #5611:

- The design-system chat threaded projectResolvedDir but neither
  projectFileNames nor an opener, so its own workspace file links were
  classified as workspace-file and then swallowed (opener undefined).
  DesignSystemDetailView now feeds ChatPane a memoized known-file set
  and an opener that switches to the Files tab before handing
  requestWorkspaceFileOpen the file — the workspace consuming the open
  request only mounts on that tab. Host-level regression added in
  DesignSystemFlow.test.tsx.

- normalizeDiskPath only recognized drive-letter absolutes, so UNC
  share workspaces (\\server\share\...) never matched a UNC
  resolvedDir and their links stayed inert. isUncPath classifies the
  backslash form (forward-slash //host/... remains excluded as
  protocol-relative), normalization keeps the //server/... shape, and
  isWindowsDiskPath folds case for both Windows shapes in the proof
  keys. isPathLikeChatHref treats unresolvable UNC hrefs as inert
  explicitly. Resolver tests cover current-project and sibling-project
  UNC cases.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-15 04:59:37 +00:00
lefarcen ad584ff901 fix(web): keep deck page counters in sync during keyboard paging (#5594)
* fix(web): keep deck page counters in sync during keyboard paging

Host-driven deck navigation (keyboard shortcuts and prev/next buttons)
used to mutate slide classes/transforms directly in the srcdoc bridge,
bypassing the artifact's own navigation script. Decks that render their
own page counter from an internal index never saw the move, so the
bottom page number stayed frozen on the first slide while the canvas
advanced. The bridge now prefers dispatching the navigation key to the
artifact's own keyboard handler and only falls back to direct DOM
mutation when no handler moves the deck.

The mirror gap: iframe-focused keyboard navigation on transform-track
decks mutates only the shared track element, which the bridge's
MutationObserver (slide attributes only) never saw, leaving the HOST
counter stale. The observer now watches the track's style too.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* worker: 使用键盘快捷键翻页的时候,下面的页码不会变动

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): wait out deferred deck key handlers before the bridge's direct-DOM fallback

Review follow-up for #5594: the artifact-key probe treated only a
synchronous index change as proof the artifact handled the key. Decks
that defer their keydown state update (requestAnimationFrame or
setTimeout) fell through to direct DOM mutation immediately AND received
the key twice — the window dispatch and the document re-dispatch both
reach a deferred window listener — so one host "next" advanced two
slides and desynced the artifact's internal index.

The probe is now asynchronous: dispatch to one target, then wait one
bounded mutation window (MutationObserver on class/style/hidden plus a
48ms cap) for the active index or track transform to change before
trying the next dispatch target or the direct-DOM fallback. Synchronous
handlers still resolve on a zero-latency fast path, and a responsive
dispatch target is cached so later keypresses skip re-probing.

Decks with no keyboard handling must not pay the probe wait: the key
path is gated on the artifact actually shipping keyboard navigation,
seeded from a source scan of the artifact html before any od bridge is
injected plus registration-time tracking through the existing
addEventListener patch — the same two-pronged pattern the od:slide
message-listener flag already uses. This preserves the pinned invariant
that the bridge-owned fallback stays synchronous for handler-less decks.

Regression fixtures: a deferred window-listening deck (red on the
synchronous probe with a visible double advance to slide 3) and
seed-scan assertions proving od-injected bridge scripts do not
contaminate the keyboard-navigation flag.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): catch external-script deck keyboard runtimes with a head-start registry hook

Review follow-up for #5594: detectArtifactKeyboardNavigation() only sees
inline HTML bytes, so decks that load their keyboard runtime via
<script src> (design-templates/html-ppt wires ../assets/runtime.js,
which registers an ArrowRight/ArrowLeft document keydown handler) were
classified handler-less. Their listeners also register before the deck
bridge executes at the end of <body>, so the bridge's own
addEventListener patches installed too late — the flag stayed false,
host-driven od:slide fell back to direct DOM mutation, and
artifact-owned counters / internal indices went stale again.

A tiny registry hook is now injected at the start of <head> — before any
artifact script — wrapping window/document addEventListener and flagging
keydown listeners that pass the shared navigation-key screen into
window.__odArtifactKeydownNavigation. The deck bridge reads that flag
lazily alongside its inline-source seed and its own late patches, so
registrations are observed no matter when they happen relative to the
bridge. The bridge marks its own top-of-file keydown listeners via
__odDeckBridgeOwnListenerInstall so the framework branch's key-token
source does not classify every framework deck as artifact-navigable.

The navigation-key screen is now a single shared snippet between the
hook and the bridge. Regression: a fixture that registers its keydown
handler before the bridge evaluates with no keydown token in the html
bytes (the script-src shape) — red on the previous commit (counter stays
1 while the canvas moves), green with the hook.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-14 08:59:13 +00:00
open-design-crew[bot] 9d055a0fa6 feat(amr): add artifact-based upgrade guidance for Free users (#5591)
* feat(amr): add artifact-based upgrade guidance

* fix(amr): fail open when billing plan is unavailable

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
2026-07-14 08:11:07 +00:00
open-design-crew[bot] 53eff935e1 feat(amr): enable Parallel Search MCP (#5552)
Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-07-14 04:39:49 +00:00
open-design-crew[bot] 6426847fba fix(web): separate run success from result delivery (#5525)
* fix(web): distinguish run success from result delivery

Persist delivery outcomes separately so successful design runs cannot report completion before a usable artifact is available.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): recover missing design delivery after reload

* chore(ci): rerun pull request checks

* fix(web): avoid reattaching local delivery finalization

* fix(web): retry delivery failures

* chore(ci): rerun pull request checks

* chore(ci): retrigger managed checks

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 10:16:04 +00:00
lefarcen bc5b6f0588 fix(daemon): flag runs that end with unfinished work so status never reads "Completed" early (#5470)
A design-generation run that ended its turn with TodoWrite tasks still
pending / in_progress / stopped — or was truncated by max_tokens — was
stamped `succeeded` and read "Completed" on the Pet task center and the
project pill, while the chat footer simultaneously read "Stopped with
unfinished work". Users trusted "Completed", assumed the work was done, and
re-requested, creating duplicate generations and eroding trust in the
run-state UI (#1247 / #1060).

Root cause: the daemon equates "the turn/process ended" with "succeeded" and
never consults the agent's own declared work; multiple web surfaces then map
`succeeded` -> "Completed" without any completeness signal, even though the
chat footer already distinguishes unfinished work.

Fix — one canonical signal, consumed everywhere so the surfaces cannot drift:

- packages/contracts/api/run-completeness.ts defines the canonical
  "unfinished declared work" predicate (status !== 'completed', so pending /
  in_progress / stopped all count), matching the web footer's
  unfinishedTodosFromEvents exactly.
- Daemon folds the last TodoWrite snapshot + a max_tokens truncation off the
  agent event stream (every runtime, via the single emitAgentEvent choke
  point) and derives run.endedWithUnfinishedWork once at finish().
- The project-status projection derives `incomplete` from the already
  persisted events_json — no schema migration — so it survives reload/reattach
  instead of flashing "Completed" once the in-memory run ages out.
- Contracts carry endedWithUnfinishedWork on ChatRunStatusResponse and the SSE
  end payload (backward compatible: absent = finished), plus a new
  `incomplete` ProjectDisplayStatus.
- Web surfaces (Pet task center, project pill, DesignsTab) render `incomplete`
  as amber needs-attention, never the success color; the footer now sources
  the same shared predicate.

Truncation uses the display-only option (spec Option A): the run stays
`succeeded` but is flagged incomplete, avoiding the code===0 / stdin-close
hazards of reclassifying a truncated turn to failed.

Regression coverage, red on main / green here: an HTTP-boundary e2e
(e2e/tests/dialog/task-completeness.test.ts) drives the interactive chat path
for unfinished / stopped / truncated / finished runs; daemon + contracts +
web unit tests pin the projection and the canonical predicate.

Ref: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/011f6f51-7a25-4043-ae94-182e0b8b0931

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-13 04:17:24 +00:00
open-design-crew[bot] 9d030e6fdf feat(chat): map long-tail failure_detail values to specific run-error cards (#5329)
* feat(chat): surface daemon failure_detail to sharpen run-error guidance

The daemon already classifies each run failure into failure_category /
failure_detail (run-failure-classification.ts) for retry policy and
telemetry, but kept it daemon-internal — so the chat could only key error
guidance off the coarse errorCode. That collapses distinct causes that
share one code: a hard quota and a transient 429 both arrive as
RATE_LIMITED, and a text-detected missing CLI leaks in as the opaque
AGENT_EXECUTION_FAILED.

Propagate the classification to the client (contract DTO + SSE end frame,
mirroring the proven `resumable` path) and let resolveRunFailureUi take
`detail` as a first-class input that can override a too-coarse code:

- hard_quota / workspace_credits_exhausted -> a distinct "Quota exhausted"
  card that drops the futile Retry (primaryAction 'none') and steers to the
  hosted-AMR switch card instead.
- cli_not_installed (text-detected) -> the same install-the-CLI card the
  AGENT_UNAVAILABLE code path already renders.

A transient 429 without a hard-quota detail still offers Retry, and the
Antigravity terminal switch-model flow still wins over the generic detail
override.

Stacks on #5308 (the code->guidance mapping this detail layer refines).

* chore(i18n): rebrand user-facing "AMR" to "Open Design Cloud"

The hosted model service is now Open Design Cloud, but the run-error
cards this PR sharpens (switch / auth / balance), plus settings account +
wallet labels, avatar console, onboarding gate, and the handoff website
link, still showed the old "AMR" (or bare "Open Design") name. Normalize
every user-visible brand mention to "Open Design Cloud" across all 19
locales. Value-only: no keys, agent ids, URLs, or raw daemon error
strings touched.

* fix(chat): persist daemon failure classification onto assistant message

The daemon emits the terminal `error` SSE frame from the child-close
handler before a run is finalized, so `failureCategory` / `failureDetail`
(computed at finalize) weren't known when that frame was first persisted
to the assistant message — the stored `status:error` event kept only
`{ detail, code }`. A failure persisted without the live web error
handler saving the message, or a conversation reloaded before that save
landed, therefore fell back to the coarse errorCode UI and lost the
specific fix guidance this PR adds.

Stamp the finalize-time classification onto the persisted assistant
message: enrich the last `status:error` event in place (append one only
if a failed run somehow never persisted an error frame). Covered by a
daemon regression that reads the stored assistant message via the
conversation-messages API after a failed hard-quota run.

* fix(chat): keep daemon failure classification on the live web reload/recovery paths

Two live-propagation gaps let the sharpened run-error card degrade back to
the coarse/raw UI even though the daemon now sends the classification:

- appendErrorStatusEvent() no-op'd whenever the message already had a
  trailing status:error with the same detail. In the reload/reattach race
  (reload reads the daemon-persisted error frame first, then the run
  finishes and onError arrives with the finalize-time classification) that
  dropped the new code / failureCategory / failureDetail. Merge those into
  the existing event in place instead; no-op only when nothing is added.

- The error-frame-then-status recovery probe in providers/daemon.ts broke
  out on a terminal failed status without copying failureCategory /
  failureDetail (unlike the post-loop status fallback), so markErrorRunFailure
  stamped null and the failureDetail mapping fell back to the generic card on
  the reconnect path. Mirror the two assignments before the break.

Covered by chat-events (in-place merge) and daemon-sse-reconnect (error
frame + terminal status fetch, no end frame) regressions.

* feat(chat): map long-tail failure_detail values to specific run-error cards

#5321 pipes the daemon's failure_detail to the chat but only mapped the
high/medium-frequency causes (hard_quota, workspace_credits, cli_not_installed).
This wires the recoverable long tail the daemon already classifies — timeout,
inactivity_timeout, empty_output, session_resume_expired, git_bash_missing —
each to a named failure type + one-line fix with a plain Retry.

These are engine-neutral outcomes, so they resolve in a new
AGENT_AGNOSTIC_DETAIL_FAILURE_UI layer BEFORE the AMR/Antigravity agent
branches (which would otherwise return a generic retry for AMR). Adds the
title/message i18n keys across all 19 locales and unit coverage for the
new mappings.

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
2026-07-13 03:49:33 +00:00
Tom Huang bc6a246c65 feat(slides): enhance deck project experience (#5083)
* feat(deck): enhance slide deck functionality with speaker notes and navigation improvements

- Added a switch for including speaker notes in slide decks, defaulting to true.
- Updated keyboard navigation to include 'R' for resetting to the first slide.
- Implemented click navigation for half-slide transitions.
- Enhanced the deck framework to support hidden programmatic prev/next controls.
- Improved handling of file inputs in question forms to support file submissions.

This update aims to improve user experience in creating and presenting slide decks, ensuring better accessibility and functionality.

* feat(artifacts): enforce semantic naming for user-facing deliverables

- Updated guidelines to require semantic filenames derived from user briefs for new artifacts, moving away from defaulting to `index.html`.
- Enhanced documentation to clarify naming conventions for HTML files, emphasizing the use of descriptive names like `investor-pitch-deck.html`.
- Adjusted prompts and hints across the system to reflect these changes, ensuring consistency in user-facing outputs.
- Improved test coverage to validate the new naming conventions and ensure proper implementation in various contexts.

This update aims to enhance clarity and usability in project file management, promoting better organization and user experience.

* feat(file-viewer): enhance speaker notes display and improve file workspace functionality

- Updated the speaker notes section to include a styled placeholder for empty notes, improving user experience.
- Changed the icon in the file workspace from "slides" to "panel-left" for better clarity.
- Introduced new sub-category filtering options in the file workspace, enhancing organization and navigation.
- Improved styling for the page creator search input and added visual feedback for loading states.

These changes aim to enhance usability and visual consistency across the file viewer and workspace components.

* test(web): align workspace tests with pages switcher

* test(e2e): open design files through pages menu

* fix(deck): keep source mode reachable

* fix(workspace): restore uploaded file tabs

* feat(deck-thumbnail): implement virtualized thumbnail rail for improved performance

- Introduced a new `DeckThumbnailRail` component to optimize the rendering of deck thumbnails, reducing memory usage and improving load times.
- Enhanced the `FileViewer` to integrate the new thumbnail rail, allowing for lazy loading of thumbnails based on viewport visibility.
- Added tests for the `DeckThumbnailRail` to ensure correct functionality and caching behavior.
- Updated styles for better visual consistency and performance during thumbnail rendering.

These changes aim to enhance user experience by providing a smoother and more efficient way to navigate through deck slides.

* feat(deck): enhance deck rendering with thumbnail support and keyboard shortcuts

- Integrated `DeckSlideThumbnail` for rendering individual slides in a shadow root, improving performance and reducing main-thread load.
- Updated `DeckThumbnailRail` to utilize the new thumbnail component, allowing for efficient slide navigation.
- Implemented keyboard shortcuts for deck navigation, enhancing user experience during presentations.
- Added tests for `buildDeckRenderInput` to ensure proper functionality of the deck stage fallback.
- Improved styling for loading states in the viewer, providing a smoother visual experience.

These changes aim to optimize the deck viewing experience by reducing resource consumption and enhancing interactivity.

* feat(deck): improve thumbnail loading and speaker notes editing experience

- Refactored `DeckSlideThumbnail` to enhance loading states with a new shimmer effect, improving visual feedback during thumbnail rendering.
- Updated `DeckThumbnailRail` to manage loading states for thumbnails, ensuring a smoother user experience.
- Simplified the speaker notes editing interface by removing the toggle switch, allowing direct editing from the notes body.
- Enhanced accessibility by adding roles and focus states to speaker notes elements.
- Added tests to verify the new loading states and speaker notes functionality.

These changes aim to optimize the user experience in deck navigation and speaker notes management, providing a more intuitive and responsive interface.

* feat(file-viewer): enhance version export functionality and speaker notes interaction

- Introduced new export options for file versions, including PDF and image formats, improving the ability to download and share content.
- Implemented a streamlined process for exporting version content, with progress indicators and error handling for a better user experience.
- Enhanced speaker notes editing by refining event handling to prevent unintended focus shifts, ensuring a smoother editing experience.
- Updated styles for improved visual consistency in the file version download interface.
- Added tests to verify the functionality of version exports and speaker notes interactions.

These changes aim to optimize the file viewer's usability and enhance the overall experience when managing and exporting project files.

* fix(web): polish export menus and creator filters

* fix(file-viewer): fall back to preview exports

* test(file-viewer): cover version export fallbacks

* test(file-viewer): assert loading skeleton by role

* fix(file-viewer): restore html source mode affordance

* fix(file-viewer): capture full version fallback snapshots

* docs: trim slides handoff eof

* test(exports): cover full preview snapshot requests

* test(file-viewer): account for snapshot options arg

* refactor(file-viewer, file-workspace): streamline export functions and enhance page creator dialog visibility

* test(file-workspace): enhance slide template creation and mock plugin responses

* feat(export): add support for versioned exports and enhance HTML export functionality

* feat(export): implement versioned export support across multiple formats and enhance related tests

* feat(file-viewer): enhance manual edit functionality with commit sequence tracking and add test for inline text session saving

* style(speaker-notes): update filmstrip and thumb-frame styles for improved responsiveness and add corresponding tests

* feat(file-viewer): enhance comment composer functionality with portal metrics and improve draw toolbar layout for mobile views

* feat(analytics): track slide deck viewing experience

Add analytics for the new deck/slides experience introduced in this PR:
the deck viewer surface, in-deck slide navigation, thumbnail rail, and
speaker-notes authoring. Presentation entry stays on the existing
present_popover click to avoid double-counting.

- contracts: DeckViewerSurfaceViewProps (surface_view),
  DeckViewerClickProps (ui_click), SpeakerNotesSaveResultProps
  (new speaker_notes_save_result event), wired into the event unions.
- web: track* wrappers + fire helpers in FileViewer — deck_viewer
  surface_view on deck mount, slide_prev/next/reset, thumbnail_select,
  thumbnail_rail_toggle (expand/collapse), speaker_notes_edit, and
  speaker_notes_save_result (preview + presenter surfaces).
- tests: deck analytics suite asserting the emitted (event, props) pairs.

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

* feat(file-viewer, file-workspace): enhance export toast functionality and improve localization for page creation

* feat(home-view, plugins-home-section, agent-skill): update default facet preference, enhance category selection handling, and improve localization support

- Set `preferDefaultFacet` to true in HomeView component for better user experience.
- Introduced `handlePickCategory` function in PluginsHomeSection to manage category selection based on card layout visibility.
- Updated PRIMARY_CATEGORIES to reflect the new order and added localization for titles and descriptions in adaptAgentSkill.
- Enhanced tests to verify the correct handling of display copy and tags from SKILL frontmatter.

* feat(plugins-home, file-workspace): introduce commercial category labels and enhance deck handling

- Added a new module for resolving and localizing commercial category labels for plugins.
- Updated the FileWorkspace and PluginCard components to display the commercial category label on deck cards.
- Enhanced the PageCreatorPresetFrame to adjust frame classes based on deck presets.
- Refactored subcategory handling in facets to align with the new commercial category taxonomy.
- Improved localization support for new category labels across multiple languages.

* feat(deck-preview): implement deck preview scaling and enhance home hero layout

- Introduced `useDeckPreviewScale` hook to manage scaling of deck previews within a fixed 1280×720 logical viewport, ensuring proportional rendering across various templates.
- Updated `FileWorkspace`, `HomeHero`, and `PluginCard` components to utilize the new scaling functionality for improved visual consistency.
- Enhanced the layout of the home hero section to accommodate new subtype chips and descriptions, including a "More" dropdown for overflow handling.
- Improved localization support for new UI elements across multiple languages.

* fix(PreviewModal): clarify sidebar prop documentation and update test description

- Updated the documentation for the `PreviewSidebar` interface to specify that the sidebar's stage-edge handles are not shown when the sidebar prop is absent.
- Modified the test suite description for `PreviewModal` to be more concise and relevant.

* test(e2e): select prototype visual card

---------

Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:41:47 +00:00
open-design-crew[bot] cd07d359df feat(chat): surface daemon failure_detail to sharpen run-error guidance (#5321)
* feat(chat): surface daemon failure_detail to sharpen run-error guidance

The daemon already classifies each run failure into failure_category /
failure_detail (run-failure-classification.ts) for retry policy and
telemetry, but kept it daemon-internal — so the chat could only key error
guidance off the coarse errorCode. That collapses distinct causes that
share one code: a hard quota and a transient 429 both arrive as
RATE_LIMITED, and a text-detected missing CLI leaks in as the opaque
AGENT_EXECUTION_FAILED.

Propagate the classification to the client (contract DTO + SSE end frame,
mirroring the proven `resumable` path) and let resolveRunFailureUi take
`detail` as a first-class input that can override a too-coarse code:

- hard_quota / workspace_credits_exhausted -> a distinct "Quota exhausted"
  card that drops the futile Retry (primaryAction 'none') and steers to the
  hosted-AMR switch card instead.
- cli_not_installed (text-detected) -> the same install-the-CLI card the
  AGENT_UNAVAILABLE code path already renders.

A transient 429 without a hard-quota detail still offers Retry, and the
Antigravity terminal switch-model flow still wins over the generic detail
override.

Stacks on #5308 (the code->guidance mapping this detail layer refines).

* chore(i18n): rebrand user-facing "AMR" to "Open Design Cloud"

The hosted model service is now Open Design Cloud, but the run-error
cards this PR sharpens (switch / auth / balance), plus settings account +
wallet labels, avatar console, onboarding gate, and the handoff website
link, still showed the old "AMR" (or bare "Open Design") name. Normalize
every user-visible brand mention to "Open Design Cloud" across all 19
locales. Value-only: no keys, agent ids, URLs, or raw daemon error
strings touched.

* fix(chat): persist daemon failure classification onto assistant message

The daemon emits the terminal `error` SSE frame from the child-close
handler before a run is finalized, so `failureCategory` / `failureDetail`
(computed at finalize) weren't known when that frame was first persisted
to the assistant message — the stored `status:error` event kept only
`{ detail, code }`. A failure persisted without the live web error
handler saving the message, or a conversation reloaded before that save
landed, therefore fell back to the coarse errorCode UI and lost the
specific fix guidance this PR adds.

Stamp the finalize-time classification onto the persisted assistant
message: enrich the last `status:error` event in place (append one only
if a failed run somehow never persisted an error frame). Covered by a
daemon regression that reads the stored assistant message via the
conversation-messages API after a failed hard-quota run.

* fix(chat): keep daemon failure classification on the live web reload/recovery paths

Two live-propagation gaps let the sharpened run-error card degrade back to
the coarse/raw UI even though the daemon now sends the classification:

- appendErrorStatusEvent() no-op'd whenever the message already had a
  trailing status:error with the same detail. In the reload/reattach race
  (reload reads the daemon-persisted error frame first, then the run
  finishes and onError arrives with the finalize-time classification) that
  dropped the new code / failureCategory / failureDetail. Merge those into
  the existing event in place instead; no-op only when nothing is added.

- The error-frame-then-status recovery probe in providers/daemon.ts broke
  out on a terminal failed status without copying failureCategory /
  failureDetail (unlike the post-loop status fallback), so markErrorRunFailure
  stamped null and the failureDetail mapping fell back to the generic card on
  the reconnect path. Mirror the two assignments before the break.

Covered by chat-events (in-place merge) and daemon-sse-reconnect (error
frame + terminal status fetch, no end frame) regressions.

* fix(web): update AMR tier-upgrade test for 3-arg resolveRunFailureUi

The main merge kept a two-argument call site that passed agentId as
detail after the signature gained an explicit detail parameter.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: re-trigger validate workspace on approved head

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: align AMR rebrand assertions with Open Design Cloud copy

Update unit and e2e selectors that still expected pre-rebrand strings
for account status, handoff website link, and switch-and-retry CTA.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 03:46:01 +00:00
open-design-crew[bot] 81b20dc6a2 fix(web): avoid missing design-system preview assets (#5381)
Co-authored-by: lefarcen <935902669@qq.com>
2026-07-09 14:34:45 +00:00
open-design-crew[bot] 28d8158994 fix(web): lower AMR low-balance warning threshold (#5349)
* fix(web): lower AMR low-balance warning threshold

* fix(web): tailor low-balance CTA for free AMR users

* fix(web): tighten AMR low-balance dialog

* fix(web): add close buttons to AMR balance dialogs

* fix(web): simplify model quality tags

* fix(web): adjust model cost labels

* chore: trigger PR CI

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
2026-07-09 11:46:11 +00:00
Marc Chan b3356807b6 fix(amr): improve unavailable model gating (#5335)
* fix(amr): honor full model list metadata

* fix(amr): classify tier entitlement failures

* fix(amr): refine unavailable model upgrade affordance

* fix(web): restore stale AMR model fallback

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

* fix(web): surface AMR tier upgrade failures

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

* fix(amr): mirror chat upgrade attribution

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

* fix(amr): skip disabled default models

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

* fix(amr): honor catalog default model

* chore(pack): bump vela cli to 0.0.20

* fix(amr): align locked catalog fallback state

* chore(nix): refresh pnpm deps hash

* fix(amr): refresh entitlement-aware model state

* fix(amr): refresh models after upgrade return

* fix(amr): preserve default model analytics

* fix(amr): tighten model gating follow-ups

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

* fix(amr): repair default and refresh follow-ups

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

* fix(amr): resolve explicit default before spawn

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

* fix(amr): concretize default model in smoke paths

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

* fix(amr): repair default model gating

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

* fix(amr): share connection-test model cache

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

---------

Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>
2026-07-09 10:37:26 +00:00
open-design-crew[bot] 461e24d20c feat(web): surface actionable run-failure guidance for common errors (#5308)
The daemon already classifies run failures into failure_category / user_action
(run-failure-classification.ts), but the chat error card only mapped ~7 codes to
clear copy — everything else fell back to the generic "Task failed" title plus
the raw upstream string, so users hitting common failures could not tell the
cause or the fix (#895).

Extend resolveRunFailureUi to name the failure type and give a one-line fix for
eight more high-frequency, clear-root-cause codes, with the raw error preserved
in the card's collapsible source area:

- AGENT_UNAVAILABLE (CLI not installed)
- AGENT_PROMPT_TOO_LARGE (shorten input / new conversation)
- AMR_MODEL_UNAVAILABLE (switch model)
- TOOL_LOOP_DETECTED (check target, then retry)
- ROLE_MARKER_HALLUCINATION (retry recovers)
- AGENT_RUNTIME_DEF_INVALID (update / contact support)
- RATE_LIMITED (non-antigravity: wait/switch, keeps AMR promotion)
- UPSTREAM_UNAVAILABLE (transient, keeps AMR promotion)

The six agent-agnostic codes resolve before the amr/antigravity branches so a
hosted run also gets the specific guidance. Adds 7 title keys + 8 message keys
across all 18 locales and updates amr-guidance unit tests.

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 07:10:11 +00:00
Tom Huang 956b6607f5 feat(preview): add powered WebGL previews and examples (#5266)
* feat(powered-preview): implement cross-origin isolation for WebGL and Worker artifacts

- Introduced a new powered preview feature that allows rendering HTML artifacts in a cross-origin-isolated iframe, enabling the use of Web Workers, WASM, and SharedArrayBuffer.
- Added API endpoint `/api/preview/isolation` to report the daemon's directly-reachable HTTP origin for rendering.
- Implemented logic in the file viewer to determine if an artifact requires powered preview based on its content.
- Updated the file serving routes to support powered preview, ensuring proper headers are set for cross-origin isolation.
- Enhanced the user interface to include new scenarios for WebGL and Worker visualizations in the home hero section, with corresponding translations and descriptions.

This feature addresses the limitations of the opaque-origin preview sandbox, allowing for more complex GPU and compute applications to function correctly.

* feat(preview): add WebGL templates and lazy HTML previews

* test(web): stabilize project back routing assertion

* fix(preview): address powered preview review blockers

* fix(preview): isolate powered preview origin
2026-07-08 08:58:51 +00:00
open-design-crew[bot] 8ca68bbfe6 feat(web): pre-run Open Design Cloud balance gate with subscription dialog (#5103)
* feat(web): Open Design Cloud pre-run balance gate — two-tier dialogs with auto-resume

Gate every AMR chat send before any run spawns. HARD tier (definitive
empty wallet <= $0, or signed out) blocks with a subscription-invitation
dialog: distinct copy and CTA per cause (console wallet page vs in-app
sign-in), benefits card, and auto-resume — a completed sign-in or a
recharge observed by a bounded wallet watch resumes the parked task on
its own. SOFT tier (<= $4) shows a dismissible reminder with top-up /
start-anyway / persisted don't-ask-again (never silences the hard tier).

Blocked project sends park in the conversation queue with their full
payload and pause auto-drain until a send passes the gate; home submits
hold the create open (continuation promise) so drafts and staged context
are never lost, and the home decision carries into the project's first
auto-send to avoid double-prompting. Fail-open contract: unknown or
unparseable wallet answers never block; hard blocks are confirmed with
refresh=1 before firing. New amr_entry sources cover both surfaces and
tiers; nine chat.amrBalanceGate.* / chat.amrLowBalance.* i18n key groups
ship in all 19 locales.

* fix(web): a stale wallet-refresh echo must not confirm a hard block

A failed refresh returns the previous cached snapshot with stale=true and
an error; treating it as a fresh definitive $0 (or signed-out) answer
violated the gate's fail-open contract and could strand a user who just
topped up while the wallet endpoint hiccuped. (Review feedback.)

* chore: retrigger ci

* test(e2e): align AMR UI checks with balance gate

* fix(web): preserve AMR gated sends across conversation switches

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: Amy <1184569493@qq.com>
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
2026-07-07 09:20:54 +00:00
YOMXXX 1b6e1e45ad fix(web): keep bare chat URLs readable (#4919) 2026-07-05 14:35:07 +00:00
Ethan Guo b3e338ddb2 test(web): cover zip encoder, design-system audit, chat-events, mentions, utils (#2451)
* test(web): cover stored-mode ZIP encoder CRC and central directory

* test(web): cover design-system audit repair-actions and chat-events dedup

* test(web): cover @mention parsing and agent label normalization

* test(web): cover error flattener, prompt-template categories, design-system sentinel

* fix(web): set ZIP UTF-8 filename flag (bit 11) for non-ASCII names

buildZip wrote UTF-8 filename bytes (TextEncoder) but left the
general-purpose bit flag at 0 in both the local and central headers, so
readers fall back to CP437 and mis-decode non-ASCII names like 日本.md.

Set general-purpose bit 11 (0x0800, EFS) in both headers when the
filename contains a non-ASCII byte; pure-ASCII archives are unchanged
(ASCII == CP437 == UTF-8), matching how Python/Go zip encoders behave.

Strengthen the UTF-8 test to assert the flag + filename in BOTH the
local and central-directory headers (parsed via EOCD), and add a case
asserting pure-ASCII names do not set the flag.

Addresses review feedback on #2451.
2026-07-02 09:29:28 +00:00
Tom Huang 6be4e56544 feat(workspace): add plan mode and Excalidraw sketch flows (#4862)
* feat(daemon): add 'plan' session mode and update related functionality

- Introduced a new session mode 'plan' alongside existing 'design' and 'chat' modes, allowing for editable document creation.
- Updated various functions and interfaces to accommodate the new session mode, including normalization and usage in commands.
- Enhanced markdown rendering capabilities by integrating micromark and its GFM extension for improved markdown processing.
- Added new input types for question forms, expanding the range of user interactions.
- Updated UI components to reflect changes in session modes and ensure proper handling of next steps based on the current mode.

* feat(chat): integrate SessionModeToggle into ChatComposer and HomeHero

- Added SessionModeToggle component to both ChatComposer and HomeHero for improved session management.
- Updated HomeHero styles to accommodate the new mode switcher layout.
- Adjusted countdown timer in QuestionsPanel to extend the skip countdown from 120 seconds to 10 minutes, enhancing user experience.
- Added tests to ensure the countdown functionality works as expected.

* feat(FileViewer): implement synchronized scrolling for Markdown editor and preview

- Added functionality to synchronize scrolling between the Markdown editor and preview panes.
- Introduced new types and utility functions for managing scroll behavior.
- Enhanced the MarkdownViewer component to handle scroll events and maintain scroll position across different modes.
- Updated the component's state management to accommodate the new scrolling features.

* feat(excalidraw): integrate Excalidraw into the project

- Added @excalidraw/excalidraw as a dependency in package.json.
- Updated vitest configuration to include an alias for Excalidraw.
- Imported Excalidraw CSS in the layout component for styling.
- Modified AssistantMessage component to handle optional projectId.
- Enhanced FileOpsSummary to display delete operations.
- Implemented new Excalidraw scene management in SketchEditor and FileWorkspace components.
- Updated MarkdownViewer to support file mentions and improved file handling.
- Refactored various components to accommodate Excalidraw integration and ensure compatibility with existing features.

* feat(files): improve sketch and file handoff flows

* test: align post-merge expectations

* chore(nix): refresh pnpm deps hash

* fix(workspace): stabilize sketch persistence and ci checks

* fix(workspace): address review blockers in editable files

* fix(workspace): persist cleared sketch scenes

* test(workspace): type sketch editor mock scene

* fix(workspace): serialize sketch autosaves

* fix(workspace): keep sketch save revisions current

* test(e2e): stabilize project workspace smoke flows

* fix(analytics): preserve plan mode for BYOK runs

* test(e2e): stabilize new project rail interactions

* fix(files): stop bash delete parsing at shell operators

* test(e2e): stabilize ui cold-start suites

* fix(viewer): preserve absolute markdown image sources

* feat(workspace): preload sketches and enhance markdown save options

- Added functionality to preload persisted sketches before opening the tab.
- Introduced new MarkdownSaveOptions type to manage save behavior.
- Updated saveMarkdownText to handle options for refreshing files and showing saving state.
- Enhanced FileViewer to maintain focus and selection during metadata refresh.
- Implemented a Toast component for user feedback on save and export actions.

* fix(web): stabilize markdown and sketch editor polish

* fix(web): finish sketch editor merge resolution

* chore(nix): refresh pnpm deps hash

* fix(plan): bypass discovery and stabilize markdown sync

* fix(web): simplify scene retrieval in SketchEditor component

* feat(web): enhance markdown viewer with auto-save functionality

- Implemented passive auto-save status in the MarkdownViewer component, replacing the manual Save button with an auto-save indicator.
- Introduced new hooks and state management for tracking auto-save events and displaying the last saved time.
- Added support for synchronized scrolling between the markdown editor and preview.
- Created a new markdown-scroll-sync module to handle scroll synchronization logic.
- Updated localization files to include new strings for auto-save messages.
- Added a SketchEnginePrewarm component to optimize Excalidraw loading times.

* feat(web): enhance session mode toggle with cost indicators

- Added cost tiers for each session mode in the SessionModeToggle component, providing users with a visual representation of usage costs.
- Introduced a new ModeCostTag component to display cost information alongside session mode labels.
- Updated localization files to include new keys for cost labels and notes.
- Enhanced styling for cost indicators to improve user experience and clarity.
- Refactored EntryShell to open the new project modal instead of creating a blank project directly from the rail.
- Implemented a utility function in markdown-scroll-sync to check for vertical progression in block offsets.

* feat(web): add max height adjustment for session mode description card

- Introduced maxHeight prop to the ModeDescriptionCard component to control the height of the description card based on available space.
- Implemented useLayoutEffect in SessionModeToggle to dynamically calculate and set the maximum height of the description card, ensuring it does not overlap with the project tab bar.
- Updated tests to verify that session modes display their expected usage/cost correctly in the UI.
- Enhanced localization files to include new cost-related strings for various languages.

* feat(web): implement goBack function for improved navigation and update auto-open logic

- Added a new `goBack` function to handle in-app navigation, allowing users to return to the previous route instead of a hardcoded destination.
- Updated the `navigate` function to maintain history state for better back navigation.
- Refactored auto-open logic to prioritize produced artifacts, allowing markdown files to be opened alongside HTML files.
- Updated tests to cover new navigation behavior and artifact selection logic.
- Enhanced localization files to include new descriptions for workspace actions.

* refactor(web): remove create design system functionality and update design files panel actions

- Removed the `onCreateDesignSystem` prop and associated button from the DesignFilesPanel component.
- Updated the empty state actions to include a button for creating a new document via the `onPaste` function.
- Adjusted tests to reflect the removal of the design system creation action and ensure the new document button is functional.
- Enhanced the MarkdownViewer component by adding a placeholder for the text area and removing the header bar for a cleaner interface.
- Updated localization files to include a new placeholder string for the markdown editor.

* fix(web): restore markdown placeholder translations

* refactor(web): streamline MarkdownViewer and enhance localization

- Removed unnecessary state management and reload functionality from the MarkdownViewer component for improved performance.
- Added a placeholder text for the markdown editor in multiple localization files to enhance user guidance.
- Updated styles for the save state indicator in the viewer to improve visual clarity and alignment.
- Adjusted tests to reflect changes in the MarkdownViewer and ensure proper functionality.

* fix(web): adjust SketchEditor button size and remove shortcut hints

- Reduced the icon size in the SketchEditor component from 13 to 12 for better alignment.
- Updated the removeSketchMermaidShortcutHints function to also remove the submit shortcut hints from the dialog, enhancing the user interface by decluttering unnecessary elements.
- Adjusted tests to verify the absence of shortcut hints in the modal after updates.

* fix(web): address plan mode follow-up polish

* fix(web): align ci expectations after merge

* test(e2e): stabilize project workspace helpers

* test(e2e): scale settings visual timeout

* fix(prompts): lock ElevenLabs voice picker choices

* test(e2e): pin project workspace P0 worker

* test(e2e): use rail new project entry

* test(e2e): relax app restoration startup waits

* fix(web): limit markdown pipe escaping to tables

---------

Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>
Co-authored-by: Amy <1184569493@qq.com>
2026-07-01 04:38:31 +00:00
PerishFire 365553fc5d [codex] Fix deck keyboard double-step navigation (#4897)
* fix deck keyboard double navigation

* fix deck slide handler detection

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* Defer deck bridge slide fallback

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* Fix self-handling deck navigation fixture

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* Keep deck bridge fallback synchronous

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* Preserve synchronous deck bridge fallback

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix deck bridge constant slide handlers

* fix deck bridge stopped-handler state sync

* Fix focused deck keyboard double navigation

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
Co-authored-by: Looper <looper@noreply.github.com>
2026-06-30 11:21:23 +00:00
lefarcen 59bca72f7e feat(export): programmatic screenshot-based PPTX/PDF export (#4604)
* feat(export): programmatic screenshot-based PPTX/PDF export

Replace the prompt-driven "ask the agent to run python-pptx" PPTX export
with a deterministic, programmatic pipeline. The daemon renders each deck
slide to a pixel-perfect PNG via the desktop's bundled Electron Chromium
(reused over sidecar IPC — no second headless engine, so the packaged app
does not grow) and assembles a one-image-per-slide .pptx (PptxGenJS) or
raster .pdf (pdf-lib).

- sidecar-proto: RENDER_SLIDES message + DesktopRenderSlides{Input,Result}
- daemon: deck-export.ts assembler (decode + pptx + pdf), POST
  /export/pptx and /export/pdf-image routes, desktopSlideRenderer wiring
- desktop: deck-capture.ts renders the deck off-screen and captures one
  PNG per `.deck > .slide` (skips presenter-mode .mini-slide clones)
- web: exportProjectAsPptx() fetch+blob download; ProjectView swaps the
  prompt path for it
- cli: `od export pptx|pdf` dual-track closure
- remove the now-dead build-pptx-export-prompt lib + test

Tests: deck-export assembler unit tests + exportProjectAsPptx web tests.
Screenshot mode ships first; editable-rebuild modes are follow-up.

* chore(nix): refresh pnpm deps hash

* fix(export): deck PDF blank pages on opacity decks + image export capturing the modal

Two pre-existing deck-export defects surfaced while validating the new export:

- Vector PDF (Print-ready) left blank pages for decks that show one slide at a
  time via opacity: DECK_PRINT_CSS forced each .slide onto its own page but
  never reset opacity/visibility, and CSS transitions animated opacity from 0,
  so inactive slides printed blank. Force opacity/visibility/animation/
  transition in print media (both the web and desktop DECK_PRINT_CSS).

- "Export as image" captured Open Design's own format-chooser modal: the host
  compositor snapshot ran after the modal opened, so the overlay leaked into
  the PNG. Capture the clean preview before showing the modal; the cached
  snapshot is reused so opening the modal never re-captures over the overlay.

* feat(export): dual-mode renderer + reuse screenshot for image/PDF export

Unify all screenshot exports on one off-screen renderer and make them
viewport-independent:

- Renderer (deck-capture.ts) now has two modes: deck → one 1920x1080 PNG per
  slide (optionally just the slide at `index`); page (no `.slide` sections,
  e.g. a website) → a single full-document PNG at natural size. Adds `index`
  to the render input and `mode` to the result.
- Image export now renders through the daemon off-screen renderer (deck → the
  current slide at fixed size; website → the whole page as one long image),
  so the exported size no longer depends on the preview pane and can never
  capture Open Design's own UI. Falls back to the host/iframe snapshot on web.
- "Export as PDF" (UI) now produces a pixel-perfect screenshot PDF that matches
  the preview (same renderer as PPTX/image), falling back to the vector
  print path on web or on failure.
- New POST /export/image route; PPTX on a non-deck returns a clear 422.

* feat(export): smart full-page image capture + PDF back to print-view default

Full-page image export now auto-selects the capture technique (users only see
"full page"):
- captureBeyondViewport (one clean off-screen pass, no fixed-element
  duplication) when the output fits the machine's real GPU texture limit —
  queried at runtime via WebGL MAX_TEXTURE_SIZE, not hard-coded — and
  below-the-fold content actually rendered.
- scroll-segment stitch otherwise (too tall, or blank-below-fold scroll-driven
  pages like parallax landings): scrolls a viewport at a time, captures each
  frame, and stitches by real scroll offset into one long PNG. RAM-bound (a
  plain buffer, not a GPU texture), capped by a memory budget; encoded with a
  tiny dependency-free PNG encoder (node:zlib) so it bundles cleanly into the
  ESM packaged main and has no Skia dimension cap.
- Output scale derives from the window DPR / actual captured chunk, fixing a
  double-scale bug (DPR x clip.scale) that produced 4x-sized images.

PDF "Export as PDF" reverts to the print-ready vector path (instant, selectable
text) as the default; the pixel-perfect screenshot PDF stays available via
`od export pdf`.

* fix(export): PDF defaults to the CJK-safe screenshot path, not vector printToPDF

Chromium's vector printToPDF embeds no fonts in the packaged runtime and drops
CJK glyphs entirely — a Chinese page exported to "PDF" lost all its Chinese text
(only Latin survived). The off-screen screenshot renderer (already used for
image/PPTX) rasterizes the real browser render, so CJK is always correct.

"Export as PDF" now produces a pixel-perfect screenshot PDF that matches the
preview (one page per deck slide, or the whole page for a website), falling back
to the vector/browser print path only on web or on failure. Verified: a Chinese
site that lost its text under vector printToPDF renders fully under the raster
path.

* perf(export): stitch scroll-segments with Electron's native PNG encoder

The scroll-segment path was slow (~100s for a long parallax page) because of a
hand-written PNG encoder: a per-pixel JS BGRA→RGBA loop over tens of millions of
pixels plus zlib.deflate of a ~110MB high-entropy buffer.

Replace both with Electron's native image pipeline: stitch chunks as BGRA with
one Buffer.copy per chunk (capturePage already returns BGRA, which is what
createFromBitmap wants — no channel swap, no per-pixel JS) and encode once via
nativeImage.createFromBitmap(...).toPNG(). createFromBitmap is a CPU bitmap, not
a GPU texture, so it is not bound by the texture limit. Removes the hand-written
PNG encoder (crc32 / chunk framing / node:zlib).

Measured on a long parallax page: image 44s→17s, PDF 101s→19s (~5x), and the
native encoder also compresses better (55MB→26MB PNG).

* fix(export): render the image only on Save, after the format is chosen

Image export was rendering eagerly when the modal opened (a holdover from when
the capture had to run before the modal to avoid catching the overlay). Now the
desktop path renders off-screen and can't see the modal, so capture moves to the
Save click: open modal → pick format → Save → render + encode + download.

The Save button shows the "Saving…" state during the render; for the web-only
host-compositor fallback the modal is hidden during the brief capture so it
can't leak into the image. The snapshot is cached, so switching format after a
render re-encodes without re-capturing.

* perf(export): raster PDF embeds full pages as JPEG (52MB -> a few MB)

The screenshot PDF embedded full-page captures as PNG, so a photo-heavy long
page produced a ~52MB PDF. Full pages now render as JPEG (quality 82) — visually
near-identical for web screenshots but ~10x smaller. Deck slides stay PNG (crisp
text/graphics); image export still uses a lossless PNG source the client
re-encodes to the user's chosen format.

Threads a `pageImageFormat` hint through the render input; the desktop renderer
encodes page mode as JPEG (CDP captureScreenshot format:jpeg / nativeImage
toJPEG) and the daemon assembler embeds with embedJpg vs embedPng per image.

* feat(export): pre-pass for reveal-on-scroll + deck image as one long image

Two improvements found while testing real landing pages:

1. Pre-pass before full-page capture: freeze animations/transitions and scroll
   the whole page once (then back to top) so reveal-on-scroll content
   (IntersectionObserver / AOS / lazy images) is triggered and holds. This is
   the standard full-page-screenshot technique. Result: pages that previously
   came back blank-below-the-fold (and fell to the duplicate-prone scroll-
   stitch) now succeed with a clean single-pass captureBeyondViewport.
   Verified: a reveal-on-scroll landing page renders fully in one shot (no black,
   no duplicates); only pure JS-scrollY parallax (which re-hides at scrollY=0)
   still falls back to scroll-stitch.

2. Image export of a deck now stitches every slide top-to-bottom into one tall
   image (the "whole deck as one picture") via the native BGRA stitch, capped so
   a long deck can't exceed the bitmap limit. Ordinary pages remain a single
   full-page capture; a specific slide index is still honored if given.

* fix(export): show the loading toast at the start (not after) + track real completion

The "Exporting" toast was set inside the promise's .then(), so for a multi-second
screenshot export it only appeared AFTER the export finished — looking frozen the
whole time. Now fireShareExport shows a loading toast immediately, clears it on
success, and shows an error toast on failure. The loading toast TTL is raised to
60s so it survives a long export, and PPTX threads its real promise (was fire-
and-forget) so the toast reflects actual completion. Adds fileViewer.exportFailed
across all locales.

* fix(export): detect scroll-driven pages by comparison, not by color

The blank-below-fold heuristic (flat-color fraction) was unreliable: a dark-
themed page that renders fine (Mindloop, 89% near-black below the fold) looked
"blanker" than a scroll-driven page that genuinely fails (Luxury, 78%), so the
black middle slipped through and exported with a black band.

Replace it with a color-independent comparison: render the document's MIDDLE
band two ways — scrolled into view (real content) vs captureBeyondViewport at
scroll 0 (what the one-shot produces). If they differ significantly, the page is
scroll-driven and we use scroll-segment stitch; otherwise the clean one-shot.
Verified: Luxury now exports full content (stitch), Mindloop stays a clean
one-shot, dark designs are no longer false-flagged.

* fix(export): recognize nested-`.slide` decks (export all slides + show PPTX)

A deck whose slides are `.slide` nested under `.deck-viewport`/`.deck-stage`
(not direct children of `.deck`/`body`) was missed: PDF/image exported only the
first slide and the PPTX option was hidden, even though the slide pager showed
"1 / 9".

- Renderer: find slides via `.slide` anywhere, filtering out presenter-mode
  clones (`.mini-slide`/`.overview`/`.thumb`) in-page, instead of the rigid
  `.deck > .slide` selector. showSlide now also sets `visibility:visible` and
  toggles the common active-slide classes (active/visible/is-active/current) so
  decks that hide via `visibility:hidden` and gate reveals on `.visible` render
  every slide; animations are frozen so reveals reach final state instantly.
- UI: PPTX export shows whenever the artifact is deck-like (incl. the
  content-detected `.slide` decks that drive the pager), matching the pager.

Verified on a nested-`.slide` deck: PPTX = 9 slides, image = 9 stitched, all
with content (previously only slide 1).

* perf(export): cache /raw/ assets + halve per-slide round trips

Two performance wins for screenshot export (covers + live preview benefit too):

1. /raw/ now emits ETag + Last-Modified + Cache-Control: no-cache, and answers
   conditional GETs with 304. Covers, live preview, and the screenshot export
   window all load project HTML + its fonts/CSS/images through /raw/, and in the
   packaged app the hidden export window shares the same Chromium session/cache
   as the web UI — so a second load reuses already-downloaded bytes instead of
   re-fetching every asset. The validators are derived from file size+mtime, so
   any agent rewrite changes them and busts the cache immediately (no-cache keeps
   it always-revalidate, never silently stale). Previously /raw/ sent no cache
   headers at all, so nothing was reusable.

2. Deck slide capture merges the slide-show DOM toggle and its two-frame settle
   into a single executeJavaScript round trip (showSlide returns the settle
   Promise) instead of two separate main<->renderer hops. Output is identical;
   the loop is measurably faster and the saving scales with slide count.

Tests: apps/daemon/tests/project-raw-cache.test.ts covers the validators, 304 on
If-None-Match / If-Modified-Since, cache-bust after rewrite, and the streamed
media path. The merge is correctness-preserving by construction (same DOM ops,
same two-frame settle).

* perf(export): hand rendered images to the daemon as files, not base64 IPC

The desktop renderer used to return every rendered slide/page as a base64 data
URL inside the JSON IPC reply. For large images (photo-heavy decks) that means a
1.33x base64 blow-up plus JSON.stringify/parse of a multi-MB string plus a
multi-MB socket transfer across the desktop->daemon sidecar bridge, with a
matching RAM spike. The pixels only exist in the desktop process (it owns the
Chromium that captures them), so they must cross to the daemon — but they should
cross as a file on the shared filesystem, not as base64 in a JSON message.

Now the daemon picks a unique scratch dir under its data root
(<RUNTIME_DATA_DIR>/export-render/<id>), passes it as `outputDir` in the
RENDER_SLIDES request, the desktop writes the images there and returns their
paths in `slideFiles`, and the daemon reads them back and deletes the dir in a
finally. desktop only ever writes to the absolute path the daemon handed it, so
this works identically in dev and packaged (desktop never infers the data root).
A unique per-request id means concurrent exports never collide. Base64 data URLs
remain a fallback for older desktop builds that don't honor outputDir.

- sidecar-proto: DesktopRenderSlidesInput.outputDir + DesktopRenderSlidesResult.slideFiles
- deck-capture: emitImages() writes files when outputDir is set (all 3 paths:
  deck per-slide, deck stitch, full-page incl. scroll-segment)
- deck-export: readSlideFiles() reads the handoff files (companion to decodeSlideDataUrls)
- import-export-routes: create/own/clean the scratch dir; prefer slideFiles

Tests: readSlideFiles unit tests; a route-level test that asserts the renderer
is handed an outputDir under the data root, the image returns, the scratch dir is
deleted after the response, and concurrent exports each get a unique dir.

* chore(export): one-line per-phase timing logs for screenshot export

A slow export now leaves a diagnosable trail instead of guesswork:

- desktop `[od-export] render`: load / assets(fonts+images) / prepare / render
  phase breakdown + total, plus mode and whether the handoff used files.
- daemon `[od-export] assemble`: renderer(IPC) / read(file handoff vs base64) /
  assemble(pptx/pdf build) + total + byte size.

These immediately surfaced that a slow image export was dominated by the
artifact's own in-browser compile (Babel/Tailwind CDN) and uncacheable external
media — not the export pipeline (file read was ~2ms). One info line per export.

* fix(export): normalize PDF page size to points; honor --title in CLI output name

Addresses review feedback on PR #4604:

- buildScreenshotPdf sized each PDF page by the captured image's pixel
  dimensions, so the nominal page size scaled with the capture's device pixel
  ratio (a 2x retina capture produced a page twice as large as 1x). Normalize
  each page to a fixed longest-side in points (960pt; a 16:9 slide => 960x540pt,
  matching PowerPoint) with the image's aspect ratio. The image still embeds at
  full pixel resolution, only the page's points change.
- `od export pptx --title "X"` forwarded the title to the server but always saved
  the local file under the source HTML's basename. Name the output after the
  slugified title when --output is not given.

Tests: PDF page-size normalization assertion (loads the PDF, checks 960pt not
the 1px capture size); sidecar-proto render-slides IPC validation (outputDir,
enum, boolean, unknown-key rejection, minimal round-trip).

* test(export): cover the server Content-Disposition filename branch

The exportProjectAsPptx happy-path test only exercised the no-header local
fallback name; production always returns a Content-Disposition. Add a test that
pins the branch the desktop download actually uses (server filename wins).

* feat(export): support arbitrary-aspect decks (not just 16:9)

Screenshot deck export no longer assumes every deck is 16:9. The renderer
measures the deck's authored slide box (the rendered rect of the first slide
with layout, so fit-to-viewport decks report the stage they actually paint),
sizes the capture window + pinned stage to it, and clips capturePage to it. The
measured pixel dimensions flow to the PPTX assembler, which derives the slide
layout from the real aspect ratio (13.333" wide, height = width/aspect) instead
of hardcoding LAYOUT_16x9 — so 4:3, square, and portrait decks export
correctly-proportioned slides and PDFs instead of being letterboxed or clipped.

Falls back to 1920x1080 / 16:9 when the slide box can't be measured or is out of
a sane range, so existing 16:9 decks are unchanged.

Verified: demo-deck measures 1920x1080 (16:9, unchanged); a 1024x768 deck
measures 4:3. Tests: PPTX layout follows 16:9 / 4:3 / 9:16 aspect (asserted via
the slide cx/cy in presentation.xml).

* fix(export): capture off-screen carousel slides (translated-strip decks)

showSlide only toggled the active class/opacity, so decks that paginate by
translating a flex-strip container (e.g. html-ppt-zhangzara-grove) left slide 2+
off-screen and capturePage kept grabbing the first viewport region — exporting
the wrong slide or a blank page.

showSlide now reports where the active slide actually landed; if it is off the
top-left capture stage, showDeckSlide restacks just that slide into the viewport
(clears ancestor transforms + pins it fixed at 0,0) and re-settles before
capture. This branch only runs when the slide is genuinely off-stage, so
transform-scaled fit-to-viewport decks (active slide already at 0,0, and which
DO rely on an ancestor scale) are never touched.

Verified: a 3-slide flex-strip carousel — slide 0 stays at 0,0 (untouched),
slides 1/2 detected off-stage (x=1920) and restacked to 0,0 before capture.

* fix(export): gate PPTX on a host runtime; unify + center the image toast

- PPTX export has no web-only fallback (it needs the daemon's Electron-Chromium
  screenshot renderer), so a web-only deployment showed a PPTX button that always
  failed with 501. Gate `showPptxExport`/`canPptx` on `isOpenDesignHostAvailable()`
  so the action only appears where it can succeed. Image/PDF keep their web
  fallbacks and stay shown.
- Image export showed an in-modal spinner and a separate, non-portaled "saved"
  toast that rendered off-center (its `position:fixed` resolved against the
  preview pane's transform). Route image export progress through the same
  portaled, viewport-centered `exportToast` used by PPTX/PDF: close the modal on
  Save, show a loading toast, then success/error — one consistent, centered toast
  style. Removes the now-dead imageExportBusy/imageExportCapturing/savedToast.

* fix(export): screenshot the current deck slide; never drop slides when stitching

Two more review findings on the screenshot export path:

- captureExportImageSnapshot() routed deck snapshots through the daemon without a
  slide index, so /export/image fell into the stitch-whole-deck branch even for
  "Copy screenshot" and "Export as image" — which both promise "the current
  preview". Pass the active slide index for decks so both capture the current
  slide. Stitching the whole deck into one long image is reserved for an explicit
  action (a follow-up modal toggle).
- stitchDeckSlides() capped the output at DECK_STITCH_MAX_H by stopping the loop
  and still returning ok:true, silently dropping trailing slides (~13+ on a 2x
  capture) — partial-success data loss. It now captures slide 0 to learn the
  native size, picks one uniform downscale so all `count` slides fit under the
  cap, and stitches every slide (long decks just get a smaller per-slide size).

* fix(export): drop dead `scale` param; keep deck PDF slides PNG (not JPEG)

Two more review findings:

- The render-slides contract accepted a `scale` field (validated in sidecar-proto,
  forwarded by handleScreenshotExport) that the desktop renderer never read — a
  broken protocol surface on the feature's first release. Remove it from the
  proto, the daemon route, and BuildDeckRenderInputOptions; the capture resolution
  comes from the measured stage size and host DPR. (No scale multiplier is needed
  today; if one is added later it must actually be applied in the renderer.)
- The deck branch derived its image encoding from `pageImageFormat`, so the
  screenshot-PDF path (which sets pageImageFormat='jpeg') made deck slides lossy
  JPEG — contradicting the contract ("deck slides stay PNG; JPEG is a full-page
  page-mode optimization") and adding compression artifacts to text-heavy slides.
  The deck branch now always encodes PNG; only `page` mode honors JPEG.

* fix(export): no silent truncation for tall pages; deterministic deck slide index

- The full-page scroll-stitch path clamped the document height to the RAM budget
  and returned ok:true, silently dropping everything below the cap on very tall
  pages. It now refuses with a clear "page is too tall — export as PDF instead"
  error instead of returning a truncated image as success; pages within budget
  still stitch their full height. (Decks downscale to fit since they are discrete
  slides; a continuous page is failed rather than seam-spliced at reduced scale.)
- Deck screenshots now always send a concrete slide index
  (slideState?.active ?? cached ?? 0) so a fresh open — or a deck detected only
  from `.slide` markup that never emits od:slide-state — captures the current
  slide instead of falling into the stitch-whole-deck branch.

* fix(export): explicit page-vs-deck signal; surface semantic export failures

Two review findings:

- Treating any `.slide` element as proof of a deck was too broad for the generic
  /export/image and /export/pdf-image routes — an ordinary page with carousel or
  testimonial `.slide` markup would skip full-page capture and stitch those
  elements as slides. The caller now passes an explicit `deck` flag (the web
  knows `effectiveDeck`; PPTX is deck-only): `deck:false` forces full-page
  capture, `deck:true` forces slide capture, and the `.slide`-count heuristic
  remains only as the no-signal fallback (e.g. the CLI).
- `exportProjectImageDataUrl()` returned null for every non-OK response, so a
  semantic failure (e.g. the daemon's new "page is too tall — export as PDF")
  was treated as "renderer unavailable" and silently downgraded to a partial
  visible-viewport screenshot. It now returns a discriminated result; the caller
  only falls back to a web capture when the off-screen renderer is genuinely
  unavailable (501/no-host/network) and surfaces the real error otherwise (Copy
  screenshot + Export as image both show the message).

Plumbs `deck` through sidecar-proto, the daemon route/options, exports.ts
(image + pptx + screenshot-pdf), FileViewer, and ProjectView. Proto test covers
deck round-trip + rejection.

* fix(export): harden the file handoff (path confinement) + narrow unavailable

Three security/contract findings on the render-slides file handoff:

- sidecar-proto now rejects a non-absolute `outputDir` (was: any non-empty
  string), so a malformed render-slides request can't make desktop main mkdir +
  write outside the daemon scratch area. Negative proto test added.
- The daemon canonicalizes every returned `slideFiles` path and requires it to
  stay under the canonical `renderOutputDir` before reading — a buggy/malicious
  renderer response can no longer make /export/{pptx,pdf-image,image} read and
  stream back arbitrary files (path traversal / symlink escape). Returns 502 on
  an out-of-scope path; handoff test proves an out-of-tree path is refused and
  its bytes never reach the response.
- exportProjectImageDataUrl wrapped the whole flow in one try/catch, so a 200
  with a corrupt/unreadable payload was reported as `unavailable` and silently
  downgraded to the viewport screenshot. The `unavailable` path is now narrowed
  to transport-level failures (the fetch itself); a bad 200 payload returns a
  semantic `error` so the real failure surfaces.

* fix(export): CLI page/deck flag; reject out-of-range slide index

Two review follow-ups:

- `od export pdf|pptx` now accepts `--deck` / `--page` and forwards the signal in
  the request body, so the CLI hits the route with the same page-vs-deck
  semantics the UI uses (which sends effectiveDeck). Previously the CLI fell back
  to the daemon's `.slide` heuristic, so an ordinary HTML file with carousel
  markup could export as a deck from the CLI but a full page from the UI. (PPTX
  stays deck-only server-side; the flag matters for PDF.) `--deck` and `--page`
  are mutually exclusive; omitting both keeps the heuristic fallback.
- renderDeckSlides rejected nothing for an out-of-range `index`: it fell back to
  range(count) and the daemon returned slide 0 with 200 for image export, so
  asking for slide 99 of a 3-slide deck silently returned slide 0. It now fails
  with a clear "slide index N is out of range" error.

* fix(export): If-None-Match precedence; renderer IPC outage -> 502 not 400

- rawRequestIsFresh fell through to If-Modified-Since even when the request sent
  a non-matching If-None-Match, so a same-second rewrite (ETag changes, but
  Last-Modified is identical at second granularity) could 304 changed bytes when
  both headers were sent. If-None-Match is now authoritative when present
  (RFC 9110 §13.1.3) — freshness is the ETag match alone. Regression test sends a
  stale ETag + the current If-Modified-Since and expects 200.
- A rejection from desktopSlideRenderer (a 600s requestJsonIpc) — missing desktop
  process, broken socket, timeout — landed in the outer catch and became
  400 BAD_REQUEST, making renderer outages look like caller errors to retries /
  monitoring. The IPC call is now wrapped and translated to 502
  UPSTREAM_UNAVAILABLE, matching the !rendered.ok branch; the outer 400 stays for
  real request-validation / assembly errors.

* fix(export): full-page stitch corrupts on fractional DPR (125%/150% scaling)

scrollSegmentStitch rounded the device pixel ratio to an integer
(`Math.round(size.width / PAGE_W)`), so on non-retina display scaling (1.25x,
1.5x) the output width and every row offset were wrong — the stitched full-page
screenshot (/export/image and the raster PDF page path) came back cropped
horizontally or with vertical gaps/overlap even though the page rendered fine.

Derive width/height/placement from the REAL captured device width and its true
(possibly fractional) ratio instead. Extracted scrollStitchGeometry /
scrollStitchRowOffset as pure helpers with a non-integer-DPR regression test
(1x / 1.25x / 1.5x / 2x).

* fix(export): broaden deck slide selector; content ETag for transformed HTML

- The renderer only recognized `.slide`, but shipped decks use other slide
  contracts the print/export path already supports (e.g.
  html-ppt-zhangzara-creative-mode uses `<section data-screen-label=...>`), so an
  explicit deck export of those silently downgraded to a single full-page
  capture. Broaden SLIDE_SELECTOR to the pdf-export family
  (`.slide, [data-screen-label], .deck-slide, .ppt-slide`), and when
  `deck === true` finds no slide surfaces, fail fast with a clear error instead
  of capturing a page.
- /raw/ revalidation used the source file's mtime ETag even when the response is
  substituted by a transform (Vite dev-entry -> dist/index.html, or preview
  bridge injection). A change to dist/index.html with an unchanged source entry
  could then return a stale 304. Compute a content ETag from the actual sent
  bytes for transformed HTML; assets/fonts/images/streamed media keep the fast
  mtime ETag + early 304. Regression: rewriting only dist/index.html returns 200.

* fix(export): gate PPTX on explicit deck; page-mode DOM intact; stitch RAM budget

Four review findings:

- PPTX action was gated on the `.slide` regex (`effectiveDeck`/`looksLikeDeck`),
  so ordinary pages with carousel/testimonial `.slide` markup surfaced PPTX and
  were forced through the deck renderer (hardcoded `deck: true`). Gate
  show/canPptx on the EXPLICIT deck signal (`isDeckArtifact`: deck renderer / kind
  / presentation) instead; real decks keep PPTX, pages don't, and `deck: true`
  is now always correct. Image/PDF stay on the broader signal (they handle pages).
- renderDeckSlides ran prepareDeck (hide chrome + freeze animations) BEFORE
  deciding page vs deck, so page-mode exports rendered on a mutated DOM (content
  using generic `.notes`/`.overview` classes vanished). Split the non-mutating
  slide count from the deck-only DOM prep; page mode now captures the original
  document.
- stitchDeckSlides capped only output height, so a wide/high-DPR deck could still
  allocate >1 GiB (8192px stage @2x => W~16384 * 30000 * 4). Add a RAM byte budget
  (320MB, like the page stitcher): downscale by min(heightScale, byteScale).
- sidecar-proto render-slides test now covers the `index` field (success + reject
  negative / fractional / non-number).

* fix(export): image/PDF deck flag from explicit signal, not .slide heuristic

The image and screenshot-PDF exports still passed `deck: effectiveDeck` (the
`.slide` regex), so an ordinary HTML page with carousel/testimonial `.slide`
markup exported only the current card instead of the full page. Drive both off
the explicit `isDeckArtifact` signal (same as PPTX): a real deck → per-slide, a
page → full-page capture. Extracted `shouldCaptureAsDeck()` as a pure helper with
a regression test (page + slides + deck:false => page, not per-slide).

* fix(export): screenshot PDF download must prompt Save As (.pdf in allowlist)

The default Export PDF flow now streams a .pdf download via
exportProjectScreenshotPdf, but the will-download Save As hook only intercepted
.pptx + image extensions — so PDF silently wrote to the OS Downloads folder
while PPTX/images prompted. Add .pdf to SAVE_AS_EXTENSIONS with a PDF filter,
and extract saveAsDialogOptionsForFilename() as a pure helper with a runtime test
(PDF/PPTX/image prompt; uppercase matched; other extensions pass through).

* fix(export): single-shot guard for image export (no double-click duplicates)

The toast-based image export closes the modal and starts the save without an
in-flight guard (the old in-modal busy/disabled states were removed), so a fast
double-click / Enter-repeat on Save could enqueue two concurrent exports
(duplicate captures, downloads, and fireImageExportResult bookkeeping) before the
modal-close re-render removed the button. Add an imageExportInFlightRef guard
that returns early on re-entry and resets in finally — mirrors the existing
screenshotInFlightRef pattern.

* fix(export): If-Range guard on /raw/ stream; block image-modal reopen mid-export

Two non-blocking correctness issues:

- /raw/ honored Range unconditionally even with the new ETag/Last-Modified, so a
  client resuming a cached font/media download after the file changed could
  splice stale + fresh bytes. Gate Range on If-Range (RFC 9110 §13.1.5): serve
  206 only when the If-Range validator (ETag or date) still matches the current
  file, else fall back to a full 200. Regression test: stale If-Range + Range
  returns 200 with the new full length.
- The image-export single-shot guard covered handleImageExportSave, but reopening
  the modal mid-export reset the shared request/result refs, mis-attributing or
  dropping the in-flight export's analytics result. openImageExportModal now
  no-ops while an export is in flight.

* fix(export): drive image/PDF deck decision off the viewer signal (effectiveDeck)

The desktop screenshot image/PDF paths were gated on isDeckArtifact while the
vector-PDF fallback (and the viewer's own prev/next/Present) use effectiveDeck.
That diverged: a metadata-free `.slide` deck rendered as a deck in preview but
exported as a single full page on a desktop host, yet as a deck via the browser
fallback — same artifact, different output depending on host.

Drive image + screenshot-PDF off effectiveDeck (the viewer's deck decision), so
export matches what the user sees and is host-independent. PPTX keeps the
narrower isDeckArtifact: it is deck-only with no vector fallback, so it can't
diverge, and it must not offer slide export for incidental carousel markup.
Removes the now-dead isDeckForExport binding.

* test(web): update image-export specs for capture-on-Save modal flow

The image-export modal was redesigned in this PR from eager-capture-on-open
(preview + live format re-render + in-modal alert + disabled-until-ready Save)
to capture-on-Save unified with the PPTX/PDF portaled-toast flow: the dialog
just picks a format, and Save closes it and runs the single capture behind the
export toast. The 9 specs in file-viewer-image-export.test.tsx still drove the
old eager flow and failed in CI (Web workspace tests). Updated each to click
Save before asserting capture, pick the format before Save, assert the portaled
toast (role=alert error text unchanged) instead of the removed in-modal alert,
and replaced the obsolete "preparing label" spec with one proving no eager
capture happens on open or on format change.

* fix(cli): od export honors the server Content-Disposition filename

The web download helper prefers the daemon's Content-Disposition filename and
only falls back to a locally derived name. `od export` ignored it and always
synthesized the name from --title/basename, so the two surfaces could write
different filenames for the same export. Parse the header (RFC 5987 filename*
and plain filename, reduced to a hardened basename so an odd header can't steer
the write outside the cwd) and prefer it when --output is not given, keeping the
title-slug/basename fallback. Mirrors apps/web/src/runtime/exports.ts.

* fix(export): detect runtime-managed decks; image=whole deck; de-dup long pages

QA found three blocking export-fidelity issues on this PR:

1. Horizontal decks export only slide 1 (image: all such templates; PDF:
   some). Runtime-managed decks (`<deck-stage>` web component with slotted
   `<section data-screen-label>` children toggled via `data-deck-active`)
   carry no literal `class="slide"`, so the viewer's `looksLikeDeck` regex
   misses them and the UI sent an authoritative `deck:false`. The host then
   force-captured page mode (`mode:'page', slides:1`) — a full-page shot of
   whatever slide was visible. PDF same path: `deck:false` skips the host
   DECK_PRINT_CSS, so decks without their own `@media print` print one page.
   Fix: a broader EXPORT-only signal `sourceLooksLikeExportableDeck` /
   `deckExportSignal` mirroring the host's slide-surface family
   (`.slide`/`[data-screen-label]`/`.deck-slide`/`.ppt-slide`) plus
   `<deck-stage>`. Kept OUT of `effectiveDeck` so the host's deck-stage-
   incompatible prev/next nav is not surfaced as a dead "— / —" control.

2. "Export as image" of a deck returned the current slide only. It now
   stitches every slide into one long image (matching the slide count the
   viewer reports); Copy screenshot / Mark-Draw capture keep the current
   slide via `captureExportImageSnapshot({ wholeDeck })`.

3. Long-page image/PDF export duplicated a fixed/sticky hero down the
   output: the scroll-segment stitch captures the viewport per offset, so a
   pinned element was copied into every segment. `preparePageForCapture` now
   neutralizes `position:fixed`->absolute and `sticky`->static before
   measuring/capturing, so each renders once (captureBeyondViewport already
   de-dupes; applied uniformly for consistency).

Red specs: exports.test.ts (deck detection), neutralize-positioning.test.ts
(fixed/sticky normalization).

* chore: re-trigger CI on updated main — needs-validation gate moved to merge_group (#4714)

* fix(sidecar): decode IPC frames with StringDecoder (multibyte UTF-8 corruption)

Exported CJK artifacts intermittently showed `???` / `◆?` (U+FFFD) in place of a
character — e.g. "拥挤" rendered as "拥���", "交付边界" as "交付���界". The bad
character varied between exports, the source bytes on disk were correct, and the
daemon /raw/ serve was byte-identical, so it was not a font or storage problem.

Root cause is in the generic JSON-IPC transport. Both the server and client
socket readers did `buffer += chunk.toString()` into a STRING. A render request
carries the full artifact HTML over the desktop IPC; when the payload spans
multiple `data` events, a multibyte UTF-8 character (CJK = 3 bytes) straddling a
chunk boundary is decoded per-chunk, turning each partial half into U+FFFD. Small
payloads never hit a boundary (hence "works in my repro, breaks on the real
file"); large real artifacts do, at whichever character lands on the split.

Fix: feed each chunk through a per-connection `StringDecoder("utf8")`, which
holds an incomplete trailing byte sequence until the next chunk completes it.

Verified end-to-end against the QA "Blog Post" artifact in a packaged client:
"拥���" → "拥挤" after the fix. Red spec: a ~1.3 MB CJK payload round-tripped
through createJsonIpcServer/requestJsonIpc (forces multi-chunk delivery) is now
byte-exact; it fails on the pre-fix reader.

* fix(export): vector deck PDF rendered only the first slide

A deck exported via the vector PDF fallback (POST /export/pdf →
exportPdfFromHtml) collapsed to a single page: only the runtime-active slide
appeared. Decks gate visibility with `.slide:not(.active){display:none!important}`
(specificity 0,2,0); the host DECK_PRINT_CSS `.slide{}` rule (0,1,0) cannot win
that cascade, so every non-active slide stayed `display:none` in print.

Fix: before printToPDF, mark every slide surface active (the same class set the
screenshot path toggles in deck-capture's showSlide), so the deck's own
`.slide.active` styling applies to all slides and DECK_PRINT_CSS paginates them
one per page. Shadow-DOM `<deck-stage>` decks are unaffected (their own
`@media print` already lays out every slide).

Verified with an offscreen printToPDF of a 12-slide `.slide`-class deck: 1 page
-> 12 pages, each a distinct centered slide.

* fix(export): screenshot PDF fails fast instead of masking errors as vector PDF

Per review: the raster-PDF path fell back to the vector `exportProjectAsPdf` for
EVERY non-ok screenshot result, so a semantic failure (bad deck routing, a 422,
a renderer-side 502, "page too tall", unreadable output) silently handed the user
a different (vector) PDF — the exact fidelity/CJK-glyph class of bug the
screenshot path exists to avoid.

exportProjectAsPptx now returns the same tri-state as exportProjectImageDataUrl:
`{ok:true}` / `{ok:false,unavailable:true}` (501 or transport — caller may fall
back) / `{ok:false,error}` (semantic — must surface). The PDF action only falls
through to the vector path on `unavailable`; a semantic error throws and is shown
in the export toast (onErr now prefers the export's own user-facing message).

* chore(nix): refresh pnpm deps hash

* fix(export): guard deck capture against stale-frame duplicate pages

QA saw a deck export with duplicate pages (e.g. two identical 目录 pages, a
slide silently missing). Root cause is a compositor race: after showing slide i,
`capturePage()` can return the PREVIOUS slide's frame when the new slide hasn't
painted yet (more likely on slower / loaded machines and slides with heavier
reveal content), so the loop emits an exact duplicate of the prior page. The
source has 12 distinct slides and live navigation is fine — the race is purely in
the offscreen capture loop.

Fix: after each capture, compare a cheap sampled checksum to the previous
slide's; if byte-identical (which can't happen for distinct slides), wait for
more frames and re-capture (bounded, 4 attempts × ~60ms). Two genuinely-identical
adjacent slides exhaust the retries and emit once. Applied to both the per-slide
(PDF/PPTX) and stitch (whole-deck image) loops.

Test: imageSignature distinguishes captures by content and length. (The race
itself is timing-dependent and not reproducible on a fast/idle machine — both
file:// and packaged-http exports of the reported deck render 12 unique pages
here — so the guard hardens the failure mode rather than relying on local repro.)

* fix(export): paginate tall pages for raster PDF instead of refusing

Per review: the single-image RAM/texture guard in capturePage refused any page
taller than the budget with "page is too tall — export as PDF instead". That is
right for /export/image, but /export/pdf-image routes ordinary-page PDFs through
the same branch — and since the screenshot-PDF path now fails fast (no silent
vector fallback), a long landing page exported as PDF hit a self-contradictory
hard error and regressed tall-website PDF export.

Fix: the PDF path (`jpeg`) now paginates a too-tall page into a multi-page raster
PDF — captureBeyondViewport per texture/RAM-bounded chunk, one image per chunk,
which the daemon assembles into one PDF page each. /export/image (png) keeps its
refusal (it has nowhere to paginate to). tallPageChunkHeights extracted + tested.

Verified offscreen: a ~20400px page → PDF path returns 3 paginated pages
(ok/page), image path still refuses.

* fix(export): capture deck slides via CDP (structural fix for duplicate pages)

Replaces the pixel-compare/retry guard (88d21c7) with a structural fix, per
review feedback: comparing each capture to the previous slide is the wrong
abstraction (it can't tell a stale frame from two genuinely-identical adjacent
slides, and wastes retries on the latter).

Root cause: the deck path used `webContents.capturePage()`, which grabs the last
COMPOSITED frame and can return the previous slide's frame when the just-shown
slide hasn't composited yet — emitting an exact-duplicate page. The page path
never had this because it uses CDP `Page.captureScreenshot`, which renders the
CURRENT DOM to a fresh frame.

Fix: deck capture now uses CDP `Page.captureScreenshot` too (attach the debugger
once around the deck loop; fall back to capturePage if it can't attach). The
captured pixels always reflect the slide just shown — no compare, no retry, no
identical-slide edge case. Animations/transitions are already frozen
(prepareDeckStage), so each slide is captured at its final state, never a
mid page-turn frame. Removed imageSignature + the retry loop.

Verified: 12-slide deck still stitches to 12 distinct slides at the correct dims.

* fix(export): current-slide capture of runtime decks uses the visible slide

Per review: deckExportSignal makes runtime-managed decks (<deck-stage> /
data-screen-label) exportable, but the current-slide path (Copy screenshot /
annotation capture) still resolved the slide index as `slideState?.active ?? 0`.
Those decks are deliberately kept out of effectiveDeck, so the viewer never
receives their active-slide bridge and slideState is null — meaning Copy
screenshot always off-screen-rendered slide 0 instead of the slide on screen,
inconsistent with the PPTX/PDF fix on the same templates.

Fix: planDeckImageCapture() decides per capture — whole-deck (Export as image),
ordinary pages, and tracked .slide decks render off-screen (with the active index
when tracked); an untracked deck's current-slide capture skips the off-screen
path and falls through to the visible host snapshot (which IS the current slide).

Tests: planDeckImageCapture unit cases (exports.test.ts) + a FileViewer
regression — Copy screenshot of a data-screen-label deck with no tracked slide
uses the host snapshot and does NOT off-screen-render slide 0.

* fix(export): don't mask post-response failures / debugger-less tall PDF as fallback

Two review edge cases:

- exportProjectAsPptx wrapped resp.blob() + triggerDownload() in the same
  try/catch that maps to `{unavailable:true}`, so a corrupt body or a
  client-side download failure (after a 200) was reported as "renderer
  unavailable" — letting the PDF caller silently downgrade to the vector path.
  Only the fetch (transport) and 501 now map to `unavailable`; post-response
  failures return `{error}` so they surface. Unit test added.

- capturePage's no-debugger fallback still returned "page is too tall — export
  as PDF instead" for the PDF path (jpeg). Pagination needs CDP, and we only
  reach this branch when the debugger can't attach, so it now surfaces a
  distinct retryable error instead of telling the user to switch to the format
  they already chose. (The debugger attaches in normal packaged exports; this is
  a rare transient.)

* fix(export): distinguish CDP attach failure from later CDP command failure

Per review: when the debugger attached but a later CDP command threw (a real
Chromium/GPU/clip error), the broad catch swallowed it and the too-tall PDF
refusal reported "renderer is busy, please retry" — hiding the actionable error
and sending users into a pointless retry loop. The retryable busy message is
only correct when the attach itself failed.

Track the caught CDP error (cdpError) separately: the too-tall PDF branch now
surfaces the real CDP error message when the debugger was available but a command
failed, and reserves the retryable "busy" message for true attach contention.

* fix(export): reject `od export pptx --page`; test the tall-PDF error split

Two review items:

- CLI: `od export pptx --page` advertised a page mode that can never work (the
  daemon forces deck mode for /export/pptx). Reject `--page` for pptx with a
  clear contract error pointing at `od export pdf --page` instead of silently
  ignoring it.

- Lock down the cdpError split with a regression: extract tooTallPdfErrorMessage
  and unit-test both branches — attach failure → retryable "busy" message;
  attached-but-CDP-command-failed → the real Chromium/GPU error surfaces (and
  neither tells the user to "export as PDF", which they already chose).

* fix(export): keep current-view captures viewport-based; reject weak If-Range

Two review items:

- planDeckImageCapture sent ordinary-page Copy screenshot / captureViewport
  annotation through the off-screen renderer (useOffscreen:true, no index), which
  renders the WHOLE document instead of the visible region — a regression for
  screenshot/annotation viewport semantics. Now: Export-as-image (wholeDeck) and
  tracked-deck current-slide still render off-screen; an ordinary page's
  current-view capture (and an untracked deck's) falls back to the visible host
  snapshot. Tests updated.

- ifRangeAllowsPartial accepted weak entity-tags for a 206, but RFC 9110 §13.1.5
  requires a strong validator and our /raw/ ETag is weak (W/"size-mtime"). A
  same-size rewrite / mtime collision could splice stale + fresh bytes under a
  matching weak tag. Now any entity-tag If-Range falls back to full 200; only the
  date form authorizes a range. project-raw-cache.test.ts pins it (weak-ETag
  If-Range → 200, fresh date → 206, stale date → 200).

* fix(export): resolve imported-folder project files via metadata.baseDir

Per review: the new screenshot export routes (and the vector /export/pdf) read
the source with readProjectFile() and no metadata, so it fell back to
<OD_DATA_DIR>/projects/:id and returned FILE_NOT_FOUND for imported-folder
projects (whose workspace lives at metadata.baseDir) even though the file renders
in the UI.

Thread project metadata through: BuildDeckRenderInputOptions and
BuildDesktopPdfExportInputOptions gain a `metadata` field passed to
readProjectFile; handleScreenshotExport and the /export/pdf route load it via
getProject(db, id)?.metadata. HTTP regression added: an imported-folder project
(created through /api/import/folder) hitting /export/image now returns 200 with
the rendered image instead of 404.

* chore(nix): refresh pnpm deps hash

* Show PPTX export for detected decks

* Fix deck export detection for page captures

* Route CLI image export through screenshot renderer

* Route legacy image export through screenshot renderer

* fix(export): per-viewport PDF pagination + parallax-faithful image capture

A long non-deck page exported to PDF came out as one giant page, and the same
page exported as an image dropped its scroll-pinned text. Both stemmed from the
page-capture path: PDF assembled one PDF page from a single tall capture, and
the image path flattened fixed/sticky positioning (fixed->absolute,
sticky->static), which deleted parallax headline/foreground text.

- PDF: add a `paginate` render-slides input. A non-deck page now captures one
  image PER VIEWPORT, top to bottom, and the daemon assembles a multi-page PDF
  (one screen per page). Decks still paginate per slide; page-mode only.
- Image: capture each viewport live at its real scroll offset and stitch into
  one tall image, keeping fixed/sticky CSS as authored -- the SAME capture logic
  as the PDF path, differing only in assembly. Drop the captureBeyondViewport
  one-shot and its isScrollBound heuristic (it rendered the whole document at
  scroll 0 and got parallax/reveal-on-scroll content wrong), and drop the
  fixed-neutralization step (it dropped pinned text).

Adds paginateViewportBand unit coverage and a paginate IPC round-trip/rejection
case; removes the now-unused neutralizeFixedAndStickyPositioning helper and test.

* fix(export): capture deck-stage at authored size; share pptx in contracts

Addresses two review findings on the screenshot export surface.

- deck-stage fidelity (blocking): the <deck-stage> runtime fits its canvas to
  the viewport with `transform: scale(...)` by default and documents that PPTX
  export must set the `noscale` attribute so the DOM is captured at the authored
  slide size. The renderer never set it, so a deck whose authored canvas differs
  from the 1920x1080 capture viewport was measured + captured at the preview-
  scaled size. prepareDeckStage now sets `noscale` on every <deck-stage> (a
  no-op for plain `.slide` decks).

- contract boundary: `pptx` was a first-class CLI/daemon export format but the
  shared `EXPORT_FORMATS` in `@open-design/contracts` still declared only
  `['pdf', 'image']`, so the capability was typed through an ad hoc local union.
  Add `pptx` to the shared contract, import it in the CLI instead of a local
  duplicate, and route `pptx` through the generic `/export` route (to the
  screenshot renderer) alongside `image`.

* fix(export): route CLI --format pdf through the raster screenshot PDF path

`od export --format pdf` still posted to the generic `/export` route, whose
desktopArtifactExporter renders vector PDF via printToPDF() and drops CJK glyphs
in the packaged runtime. The web UI was deliberately switched to the raster
`/export/pdf-image` path for that reason, so the CLI diverged from the UI on the
exact decks/pages this feature targets.

Route all three CLI formats through the screenshot renderer (pdf →
/export/pdf-image, matching the UI). Extract the format→route mapping into a
pure `exportRoutePath` helper so it is unit-testable without executing the CLI
entrypoint, and assert no format falls through to the vector `/export` route.

* fix(export): route generic POST /export pdf through the raster screenshot path

The shared ExportRequest contract advertises `pdf` as part of the screenshot-
rendered export surface, but the generic `/export` route still sent `format:
'pdf'` to desktopArtifactExporter's vector printToPDF() path, which drops CJK
glyphs in the packaged runtime. So a contract caller hitting POST /export got the
lower-fidelity PDF while the dedicated /export/pdf-image route, the UI, and the
CLI all use the raster screenshot PDF — the API surface was internally
inconsistent.

Route every /export format (pdf included) through handleScreenshotExport so the
generic endpoint matches the dedicated routes and the contract; drop the now
unused desktopArtifactExporter / buildDesktopArtifactExportInput wiring from the
route. Add an HTTP-level regression asserting POST /export with format:'pdf'
runs the screenshot renderer and streams back a real (%PDF) raster PDF.

* Restore editable PPTX export

* Clarify authored slide measurement

* Enable PPTX export from browser

* Stabilize large editable PPTX text

* Use workspace root for PPTX export resource

* Let CLI exports auto-detect decks

* Avoid tracking generated PPTX bundle

* Fix generic export deck routing

* Fix deck export routing regressions

* Add CLI page-mode export flag

* Preserve authored deck capture DOM

* Load PPTX vendor bundle from gzip resource

* Harden export CLI and PPTX bundle loading

* Preserve editable PPTX slide background images

* Preserve export render sizing contract

* Classify screenshot export request errors

* Preserve freeform slide deck exports

* Preserve UTF-8 export filenames

* Align export routing and CLI JSON contract

* Preserve export compatibility paths

* Keep PDF export on screenshot renderer

* chore(nix): refresh pnpm deps hash

---------

Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>
2026-06-29 13:15:14 +00:00
Tom Huang 29b138f7a3 feat(brands): turn any brand into a reusable design system (#4691)
* Implement brand management routes and CLI support

- Added `brand-routes.ts` to handle HTTP endpoints for brand operations: listing, extracting, retrieving details, deleting, and serving logos.
- Introduced `brands-cli-help.ts` for CLI commands related to brand management, including usage instructions for listing, creating, retrieving, and deleting brands.
- Updated `cli.ts` to integrate brand commands into the existing CLI structure, allowing headless management of brands via the command line.
- Created supporting files for brand metadata handling, including `design-md.ts` for rendering brand information in markdown format and `index.ts` for the brand engine API.
- Implemented `prefetch.ts` to fetch and process brand material from specified URLs, ensuring a streamlined extraction process.
- Enhanced server setup in `server.ts` to register brand routes and manage brand-related data effectively.

This commit establishes a comprehensive framework for managing brands within the application, facilitating both HTTP and CLI interactions.

* Enhance memory management and onboarding experience

- Introduced canonical profile labels to ensure consistent handling of user input in profile forms, preventing duplicate entries.
- Updated the `parseProfileBody` and `captureProfileFromForm` functions to utilize the new canonical label matching.
- Added a memory callout section in the onboarding view to highlight the benefits of memory usage, including personalized responses and reduced setup questions.
- Implemented new UI elements in the onboarding view to improve user engagement with memory features.
- Expanded i18n support for new onboarding messages related to memory benefits across multiple languages.

* Refactor onboarding flow and enhance design system integration

- Updated the onboarding process to include a new brand extraction step, replacing the previous newsletter step.
- Adjusted the tracking logic to reflect the new onboarding steps, ensuring accurate analytics for user progress.
- Improved the UI for the onboarding view, including new input fields for email collection during the brand extraction phase.
- Refined the EntryShell component to remove outdated comments and clarify the onboarding renderer's purpose.
- Enhanced CSS styles for the onboarding steps to improve layout and user experience.
- Updated internationalization strings across multiple languages to reflect changes in the onboarding flow and brand extraction messaging.

* Add brand management features and enhance font handling

- Introduced new modules for managing brand assets, including `chrome.ts` for headless Chrome operations and `fonts.ts` for self-hosting web fonts.
- Implemented `prefetch.ts` to streamline the brand material extraction process, allowing for efficient harvesting of colors, fonts, and logos.
- Enhanced the brand system with new schema definitions in `schema.ts` to support brand color and font management.
- Developed the `engine` module to integrate brand building and rendering processes, including token derivation and artifact generation.
- Improved the overall structure and organization of brand-related files for better maintainability and scalability.

* Enhance brand extraction and project management features

- Updated `brand-routes.ts` to include new dependencies for project management, allowing for the registration of brand-related projects.
- Modified the `extractBrand` function to support project ID and system files, improving the brand extraction process.
- Enhanced the CLI commands in `cli.ts` to handle project IDs during brand creation, enabling better tracking of brand projects.
- Updated the server setup in `server.ts` to register new project-related routes.
- Improved the UI components to display project information associated with brands, including buttons for opening projects in the `BrandDetailView` and `BrandsTab`.
- Added new metadata fields in the contracts to support project tracking and management for brands.

This commit establishes a more robust framework for managing brand projects, enhancing both backend and frontend functionalities.

* Enhance onboarding profile management and memory persistence

- Added new canonical profile labels for 'Organization size', 'Use cases', and 'Discovery source' to improve user input consistency.
- Introduced `OnboardingProfileState` type to manage onboarding profile data more effectively.
- Implemented functions to build and persist the onboarding profile body to memory, ensuring user selections are saved accurately.
- Updated the `OnboardingView` component to handle profile persistence during navigation and submission steps.
- Enhanced tests to verify that user selections are correctly persisted to the memory profile.

This commit improves the onboarding experience by ensuring that user inputs are consistently captured and stored, enhancing overall user engagement with the application.

* Reflow brand extraction into an agent-driven, live flow

Replace the deterministic SSE prefetch/preview/system pipeline with an
agent-driven extraction: POST /api/brands now reserves the brand and stands
up a backing project with the target site open in an in-app browser tab plus
a seeded prompt, so the agent measures, synthesizes brand.json incrementally,
and the user can clear anti-bot walls by hand. New /preview and /finalize
routes let the agent render the kit page live and register the resulting
user:<id> design system, so extracted brand facts persist as a structured,
reusable brand kit instead of a one-shot deterministic guess.

Adds the brand-extract skill (SKILL.md + brand-kit.html template), kit-render
engine, brand-extraction-engine tests, brand project covers in the Designs
tab, onboarding extract handoff, and the matching od brand extract/preview/
finalize CLI subcommands and contract updates.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Sediment finalized brands into structured memory

Reflow a finalized brand into the memory store (brandToMemoryEntries +
reflowBrandToMemory) so future chats can ground vague requests in the
brand's palette, type, voice and rules. finalizeBrand now wires through
the runtime dataDir and best-effort persists the brand, MemoryChangeEvent
gains a 'brand' source, and the brand kit render hardens its inline JSON
escaping. Adds brand.previewEmpty / brand.viewDetails across all locales.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Implement logo fallback and imagery support in brand extraction

- Introduced a deterministic logo fallback mechanism to ensure that brand extraction processes can retrieve and save site logos, even when the agent fails to do so.
- Enhanced the `startBrandExtraction` and `finalizeBrand` functions to utilize the new logo fallback, allowing for better handling of logo assets.
- Added support for imagery samples in brand validation, enabling the inclusion of representative images in the brand kit.
- Updated the brand kit rendering to include self-hosted fonts and imagery, improving the overall presentation of brand assets.

This commit strengthens the brand extraction workflow by ensuring that logos and imagery are reliably captured and displayed, enhancing the user experience in brand management.

* Enhance memory management with rule proposal and verification features

- Introduced new functionality for distilling annotations into rule proposals, allowing users to suggest rules based on in-canvas annotations through the `od memory rule suggest` command.
- Implemented a verification system that programmatically enforces compliance with active rules during artifact generation, ensuring that all active rules are covered in the self-verify scorecard.
- Added endpoints for managing verification outcomes, including listing, removing, and clearing verification records, enhancing the transparency of the verification process.
- Updated the memory management system to support the retrieval of active rule entries, ensuring that only linked rules are considered during verification.
- Enhanced tests for both rule proposal generation and verification processes to ensure reliability and correctness.

This commit strengthens the memory management capabilities by integrating rule proposals and verification, improving the overall user experience in managing design rules and ensuring compliance.

* Distill review annotations into memory and enforce self-verify scorecard

Add distillAnnotationsToMemory to mine inline preview comments/highlights/
marks into durable feedback + rule memory via a dedicated distiller prompt,
threaded through the existing extract pipeline with an 'annotation' change
source. Tighten the self-verify prompt (daemon + contracts) to state the
daemon programmatically checks the scorecard, so a missing or uncovered
scorecard on an artifact turn is an enforcement failure. Cover the rule
suggest and verification-history routes with tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply brand design system through web config on "Use in new chat"

Thread onApplyDesignSystem from the entry shell into BrandsTab so the brand's
registered design system is applied via the web config channel instead of a
bare daemon PATCH that left the Home composer stale. Add a transient
home-intent latch + event so the Brands tab can request the Prototype chip on
the already-mounted HomeView, which consumes it once the plugin catalog loads.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Wire annotation distillation into background memory extraction

Add a background distill pass that mines inline review annotations
(comments / highlights / drawn marks) from a turn into durable memory
alongside the general LLM extraction, surface an `annotation` memory
toast source in the web UI, and cover the flow with a unit test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix brand design system not applying to composer on "Use in new chat"

Selecting a brand's "Use in new chat" applies the brand's design system as
the default and fires the Prototype chip intent in the same synchronous click
handler. HomeView consumed that intent inside the event listener, so `pickChip`
ran before React committed the config change and seeded the composer's
design-system field from the stale (empty) default — the composer showed
"No design system" instead of the brand until a reload.

Split the intent handling: the listener now only bumps a tick, and a separate
effect consumes the chip after the re-render lands, so the seeded design system
reflects the freshly-applied brand. Add the previously-untracked home-intent
latch test coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): rework Brands into Brand Kit and add Home create entry

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(brands): harvest real cover/hero images for the Images module

The brand kit's Images gallery only populated when the extraction agent
remembered to save imagery — so a forgetful or bot-blocked agent (and the
pre-imagery "Open Design" brand) left it empty. Add a deterministic,
server-side imagery fallback (imagery-fallback.ts), mirroring the logo
fallback: it parses og:image/twitter:image, large <img> (highest-res
srcset/<picture>), <link rel=preload as=image>, and CSS background-image
hero blocks, fetches candidates with browser-shaped headers, decodes
PNG/GIF/JPEG/WebP dimensions to keep only big representative images
(dropping icons/sprites/logos/tracking pixels), dedupes by content hash,
and saves up to 8 of the largest into imagery/ with labeled samples.

finalizeBrand runs it as a timeout-bounded, failure-tolerant safety net
(injectable so tests stay offline) when the agent captured too few
samples, first adopting any on-disk images. The extraction prompt and
brand-extract SKILL now explicitly direct the agent to harvest the site's
large/cover/hero images, filtered by rendered size.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(qa): implement deck layout validation and safety checks

Add a new QA module for validating the layout of generated brand decks to ensure robustness against clipping and truncation issues. The `analyseDeckLayout` function checks for critical layout invariants, including the presence of `.slide` sections, correct container types, and necessary runtime layers. Introduce `assertDeckLayoutSafe` to enforce these checks during brand system rebuilds, preventing the deployment of decks that fail validation. Additionally, create comprehensive tests to verify the functionality of the new layout validation features.

* fix(brands): apply deck shrink-to-fit synchronously so slides never clip

The no-clip runtime scheduled its fit pass through requestAnimationFrame,
whose callbacks are throttled while the deck is offscreen or occluded. A
slide could therefore stay unscaled — and clip its content — until first
paint. Fit synchronously on resize/load/fonts-ready with a trailing
setTimeout settle pass for late reflow, removing the rAF dependency.

Verified at the previously-broken 1024x620 viewport: container-type:size,
zero truncations, runtime auto-applies scale (Problem 0.71, ASK 0.87,
Product 0.97, Competition 0.97) and frame clip count drops to 0.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): let New Brand modal embed scrollable brand reference picker

Add a fillHeight mode to BrandReferencePicker so the heading, quick-pick
row and controls stay pinned while only the gallery scrolls inside a
bounded-height parent. Wire it into NewBrandModal with a stable, spacious
dialog and refresh the related newBrand/brandPicker copy across all 18
locales.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(brands): enhance brand extraction with deterministic seed harvesting

Introduce a new `seed-fallback` module to provide a server-side deterministic palette and typography seed during brand extraction. This ensures that the brand kit's initial display includes a harvested logo, an approximate color palette, and font families, improving the user experience by reducing the all-skeleton appearance during the first paint. Update the `startBrandExtraction` function to utilize this new module, allowing for a more seamless and visually appealing brand extraction process.

Additionally, enhance the `BrandReferencePicker` component to reflect loading states and errors during brand extraction, ensuring users receive immediate feedback on their actions. Update related tests to verify the idempotency of the `finalizeBrand` function, ensuring that re-finalizing a brand correctly reuses the existing design system without duplication.

* feat(brand-extract): enhance BrandReferencePicker and localization updates

Updated the BrandReferencePicker component to reflect loading states and errors during brand extraction, improving user feedback. Added a new localization key for the brand extraction process and updated existing translations in English, Simplified Chinese, and Traditional Chinese to enhance clarity and user experience. Additionally, introduced new styles for better interaction with brand assets in the brand kit template.

* feat(brands): wire in-page lightbox/masonry/asset preview + refine seed

Brand-kit preview improvements for the live extraction kit:
- brand-kit.html: add in-page overlay system (sandboxed iframe has no
  top-nav) — clickable image lightbox with prev/next, a "view all"
  masonry modal, and a full-page asset preview modal that loads
  system/artifacts/<kind>.html in an iframe. Defer auto-reload while an
  overlay is open so it never yanks the modal out mid-interaction.
- seed-fallback.ts: prefer vivid mid-luminance hues for the seeded
  accent/accent-secondary, and drop icon/symbol faces (Remix Icon etc.)
  from the typography seed so specimens never render glyph soup.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(brands): wire in-page lightbox/masonry/asset preview + refine seed

Brand-kit preview improvements for the live extraction kit:
- brand-kit.html: add in-page overlay system (sandboxed iframe has no
  top-nav) — clickable image lightbox with prev/next, a "view all"
  masonry modal, and a full-page asset preview modal that loads
  system/artifacts/<kind>.html in an iframe. Defer auto-reload while an
  overlay is open so it never yanks the modal out mid-interaction.
- seed-fallback.ts: prefer vivid mid-luminance hues for the seeded
  accent/accent-secondary, and drop icon/symbol faces (Remix Icon etc.)
  from the typography seed so specimens never render glyph soup.

Co-authored-by: Cursor <cursoragent@cursor.com>

* i18n(web): add brandPicker.opening across remaining locales + picker test

Completes the brand-reference picker i18n key that was committed only for
en/zh-CN/zh-TW, so every locale satisfies the typed Dict, and lands the
BrandReferencePicker extraction-feedback test left untracked by the
concurrent worker.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(EntryShell): enhance AMR cloud card visibility post-detection

Updated the EntryShell component to ensure the AMR cloud card remains visible after detection settles, even when the AMR runtime is unavailable. This change prevents the card from disappearing and allows it to degrade gracefully to fallback content and sign-in flow. Additionally, added tests to verify the new behavior, ensuring a better user experience during onboarding.

* feat(library): OD Library asset registry + OD Clipper extension

Add a global, cross-project asset registry (OD Library) and a Chrome MV3
capture extension (OD Clipper), wiring the full HTTP + CLI + Web UI three-track
loop per specs/od-clipper.md.

- contracts: LibraryAsset/Source/Kind, ingest, search, pairing, task DTOs
- daemon: 6 additive SQLite tables, content-addressed owned storage, the
  idempotent registerLibraryAsset hook (hash dedup + append-source),
  programmatic enrichment (mime/size/image dims/domain/tags), pairing tokens
  with a persisted extension-origin allowlist, /api/library/* routes, and
  /api/tools/library/{search,apply} for in-task agent reuse
- cli: `od library list|get|rm|search|import|pair`
- web: Library tab (grid, source badges, filters, search, live SSE updates,
  extension pairing affordance)
- clipper/: standalone MV3 extension (background SW, content toolbar, popup)
- skills/library-curator: utility skill for agent-driven asset reuse

Origin middleware now honors paired chrome-extension:// origins (seeded from
SQLite on boot) and exempts the pairing-confirm handshake. Enrichment AI stages
(caption/OCR/embedding) are recorded as skipped pending a configured model.

* feat(brands): programmatic-first design system extraction + rename

Make brand extraction two-phase so a usable design system is ready the
moment the user enters a URL — the instant "aha" — instead of waiting on
the AI agent:

1. PROGRAMMATIC-FIRST (synchronous): startBrandExtraction now harvests the
   site deterministically (logo, palette, typography, one-line description,
   cover imagery, source URL) via prefetchBrand, synthesizes a valid design
   system with brandFromMaterial (no LLM), and finalizes + registers it
   before returning. finalizeBrand is refactored into a reusable
   finalizeBrandCore shared by both the programmatic path and the agent path.
2. ASYNC AI ENRICHMENT: the seeded agent prompt is reframed to enrich the
   already-usable design system and re-finalize in place (same user:<id>),
   updating every artifact/template.

Bounded + best-effort: a blocked/unreachable origin skips phase 1 and stays
`extracting` for the agent to drive. Gated on userDesignSystemsRoot so the
legacy agent-only path stays intact for tests.

Also rename the user-facing "Brand Kit" surface to "Design System" across
en + zh-CN strings, project names, and the enrichment prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(library): enhance asset import and management features

- Updated the `import` command to allow multiple local files and remote URLs, with restrictions on supported formats.
- Added new commands: `apply` for copying assets into project design files, `edit-as-page` for converting HTML assets into editable projects, and `figma` for exporting Figma captures.
- Introduced sidecar functionality for storing derived data alongside owned assets, including Figma capture IR and element HTML.
- Enhanced server configuration to support larger ingest payloads for asset captures.
- Improved error handling and user feedback during asset import and application processes.

* feat(asset-management): enhance asset dropzone and introduce chat-to-design feature

- Updated the DesignSystemAssetDropzone component to improve file preview handling with new functions for creating and revoking object URLs.
- Adjusted CSS for better layout and spacing in the asset dropzone.
- Added a new "Chat to design" button in the LibrarySection component, allowing users to send selected assets to the Home chat composer for project creation.
- Updated localization strings across multiple languages to reflect changes in asset import terminology.
- Enhanced the HomeView component to handle asset staging from the chat composer.

* feat(library): enhance asset application with element markup support

- Updated the `applyLibraryAsset` function to include an `includeElement` option, allowing the capture of element markup alongside assets.
- Modified related components (e.g., `ChatComposer`, `LibrarySection`, `FileWorkspace`) to handle the new element markup feature, ensuring both asset paths and optional element paths are returned and processed.
- Introduced a new function, `fetchLibraryAssetElementHtml`, to retrieve the captured HTML for element-pick assets.
- Enhanced the UI to display element markup inline within the chat composer, improving user interaction with captured elements.
- Updated API contracts to reflect changes in asset application responses, including optional element markup paths.

* feat(library): enhance asset filtering and preview handling

- Updated the LibraryPicker and LibrarySection components to implement a badge-aware kind filter, allowing for more precise asset filtering based on badge kind.
- Introduced a new `matchesKindFilter` function to streamline the filtering logic across components.
- Enhanced the DesignSystemAssetDropzone to ensure proper handling of image previews, addressing issues with broken thumbnails under React StrictMode.
- Added CSS styles for kind badges to improve asset representation in the UI.
- Implemented tests for the DesignSystemAssetDropzone to ensure correct preview lifecycle management.

* feat(library): hydrate single asset on SSE ingest

Add fetchLibraryAsset(id) so the Library grid can merge just the one
asset an `ingest` SSE event references instead of refetching the whole
list on every capture. Returns null on miss/error.

* feat(clipper): richer in-page image picker

Collect CSS background-image url()s in addition to <img> (so hero/section
art painted as backgrounds is no longer silently missed), defer thumbnail
decode to visible cells via IntersectionObserver, draw downscaled canvas
thumbnails instead of second full-res decodes, and add locate-on-page
highlighting so a picked image can be traced back to its DOM source.

* feat(library): implement lazy loading for thumbnails and enhance asset filtering

- Introduced a `LibraryThumb` component to lazily load heavy content (images, videos, iframes) only when they are near the viewport, improving performance.
- Added a debounced search feature to optimize asset filtering, reducing unnecessary network requests during rapid input.
- Enhanced the asset filtering logic to track active filters using a ref, ensuring efficient updates during live events.
- Updated the `snapshotCardRects` and `cardIdsInBand` functions to support improved hit-testing for drag-and-drop interactions.

* feat(library): lazy picker thumbnails + debounced search

Extend the Library grid's lazy-thumbnail + 250ms debounced-search pattern
to the composer LibraryPicker so opening it no longer fires one full-bytes
request per asset, and tidy the clipper content-script image collection.

* feat(clipper): compress and budget capture inlining

Re-encode large raster images to downscaled WebP and inline smallest-first
within a fixed budget, dropping only the secondary Figma IR past a safe body
size, so an image-heavy page (e.g. a news front page) always saves as an
editable HTML capture instead of 413-failing the ingest.

* test(library): LibraryPicker debounce + lazy-thumbnail coverage

Cover the composer picker's 250ms debounced search and its lazy <img>
mount (deferred until the card is in view), matching the grid's perf test.

* feat(design-system): enhance asset handling and UI for design systems

- Updated the CLI to support additional asset kinds, including 'design-system'.
- Enhanced the DesignSystemProvenance type to include source URLs, improving provenance tracking.
- Modified the design system generation jobs to correctly summarize source links and GitHub repositories.
- Updated UI components to reflect changes in asset handling, including new source link management in the DesignSystemFlow.
- Improved tests to cover new functionality for adding source links and ensuring proper handling of design system assets.

* refactor(library): rename 'design-system' to 'brand kit' and enhance thumbnail loading

- Updated labels and filters in Library components to replace 'design-system' with 'brand kit'.
- Introduced a shimmer skeleton for lazy-loaded thumbnails in the LibraryPicker to improve user experience during asset loading.
- Enhanced the PickerCard component for better performance by memoizing individual asset cards.
- Updated tests to ensure proper handling of brand kit assets and their visibility in the LibraryPicker.

* feat(clipper): implement internationalization for toolbar and popup

- Added i18n support to the clipper, enabling localization of UI elements and tooltips.
- Introduced a new i18n.js file to manage translations for various languages.
- Updated content.js and popup.js to utilize the i18n functions for dynamic text rendering.
- Enhanced accessibility by ensuring aria-labels and tooltips are also localized.
- Improved user experience by providing localized messages for actions and statuses.

* feat(clipper): enhance brand kit extraction and localization support

- Updated the brand kit extraction process to include improved handling of assets and localization for various UI elements.
- Added internationalization support for the brand kit feature, allowing for dynamic text rendering based on user locale.
- Enhanced the user experience by ensuring that all relevant messages and tooltips are localized.
- Updated tests to cover new localization features and ensure proper functionality of the brand kit extraction process.

* feat(clipper): enhance brand color derivation and update localization

- Introduced new functions for color manipulation, including linear interpolation and clamping, to improve brand color derivation.
- Updated the deriveBrandColors function to better map observed palettes to semantic roles, ensuring consistent brand representation.
- Revised localization strings in i18n.js to reflect changes from 'brand kit' to 'design system', enhancing clarity and user experience.
- Improved overall code organization and readability by refactoring existing functions and adding new utility methods.

* refactor(clipper): update terminology from 'brand kit' to 'design system'

- Replaced all instances of 'brand kit' with 'design system' across various components and localization files for consistency.
- Updated UI elements, tooltips, and documentation to reflect the new terminology.
- Enhanced user experience by ensuring clarity in the design system extraction process and related functionalities.
- Adjusted localization strings in multiple languages to align with the updated terminology.

* feat(clipper): enhance image fill handling and normalization

- Introduced functions to normalize image fills by converting non-PNG/JPEG formats (SVG, WebP, GIF, AVIF) to PNG before import, ensuring all images are properly rendered in Figma.
- Updated the UI to report the number of images converted and dropped during the import process, improving user feedback.
- Enhanced the overall image processing workflow to prevent silent failures when unsupported formats are encountered.
- Revised documentation to reflect the new image handling capabilities and supported formats.

* feat(clipper): enhance UI kit and busy state feedback

- Updated the UI kit to include new components such as inputs, selection, and overlays, improving the overall design system representation.
- Enhanced the busy state feedback during capture processes with localized messages and a step-by-step progress indicator, providing users with clearer status updates.
- Revised localization strings to support new UI elements and improve user experience across multiple languages.
- Improved documentation to reflect changes in the UI kit and busy state handling.

* fix(brands): restore design-systems nav entry + reconcile BrandsTab on re-activation

Address review feedback on PR #4260:

1. EntryNavRail dropped the only control that reached view==='design-systems'
   when Brands replaced it in the rail, leaving the still-rendered/routed
   design-system manager deep-link only (the entry-nav-design-systems e2e
   specs assert this). Restore a reachable rail entry (blocks icon, existing
   navDesignSystems key) alongside Brands.

2. BrandsTab only fetched once on mount, but EntryShell keeps sub-views
   mounted and toggles visibility, so a brand finishing extraction in its
   backing project never reconciled until a full reload. Refresh whenever the
   Brands view becomes active again, and poll while any brand is extracting
   (torn down once settled / when hidden).

Red spec: tests/components/BrandsTab.refresh.test.tsx (fails pre-fix:
fetchBrands called once, not twice).

* Update clipper/brand-capture.js

* fix(clipper): improve busy state handling and UI feedback

- Adjusted the spinner CSS to use flex properties for better layout control.
- Enhanced the reclampIfMoved function to preserve user position during busy state transitions.
- Added loading toast notifications for popup-launched captures to ensure progress visibility even when the on-page bar is hidden.

* feat(daemon): add kiwi-schema dependency and enhance Figma API integration

- Added kiwi-schema package to the daemon for improved schema handling.
- Updated FigmaApiNode interface and related functions to support shared functionality with the offline decoder, ensuring consistency in node processing.
- Refactored capture functions in clipper to maintain UI visibility during DOM/IR snapshots, enhancing user experience during capture operations.

* fix(web): surface missing backing projects

* fix(web): re-enable brand actions after use

* fix(daemon): serve brand logos from data roots

* fix(brands): reconcile failed extractions

* feat(daemon): implement offline Figma import and decoding functionality

- Added support for importing `.fig` files directly into the daemon, enabling offline processing without requiring a Figma account.
- Introduced a new `fig-decode.ts` module for decoding `.fig` files, handling both ZIP-wrapped and raw formats.
- Created `figma-import.ts` to orchestrate the import process, generating a canonical snapshot and associated metadata.
- Enhanced the server to handle Figma file uploads and integrate with the new decoding logic.
- Updated package dependencies to include `kiwi-schema`, `html2canvas`, and `jspdf` for improved functionality.
- Added tests for the new Figma import features to ensure reliability and correctness.

* feat(clipper): reload-proof capture progress badge on the extension icon

The on-page progress strip dies if a page reloads itself mid-capture
(aggressive paywall sites like economist.com do this), leaving no
loading signal. Add a per-tab '•••' badge on the extension icon for the
lifetime of any capture message — it lives on the action icon, so a page
navigation can't take it down. Verified end-to-end via a real loaded
extension.

* feat(daemon): add export functionality for Figma and enhance PDF export process

- Introduced `runFigma` command for importing Figma designs, supporting both local `.fig` files and Figma URLs.
- Added detailed usage instructions for the `od figma import` command.
- Implemented `runExport` command for programmatic export of HTML/deck artifacts to PDF, PPTX, or image formats.
- Enhanced error handling and user feedback during export processes.
- Removed obsolete `build-pptx-export-prompt` module and related tests to streamline the codebase.

* feat(daemon): enhance library synchronization and export capabilities

- Implemented `reconcileLibrary` to mirror design systems and agent-produced project deliverables into the Library as referenced assets.
- Added support for programmatic export of artifacts via the `od export` command, including detailed usage instructions.
- Introduced new functions for handling Figma imports and exports, improving integration with design workflows.
- Enhanced error handling and user feedback during synchronization and export processes.
- Added tests for new features to ensure reliability and correctness.

* feat(web): PPTX export for any shareable artifact + Library toolbar tooltips

* chore(nix): refresh pnpm deps hash

* refactor(web): enhance onboarding view and file export progress indicators

- Updated the onboarding view layout for improved accessibility and visual hierarchy, including adjustments to spacing, typography, and button styles.
- Introduced a loading toast for file export progress, displaying elapsed time and estimated time remaining for slide exports.
- Added new translation keys for export progress messages in multiple languages.
- Refactored the export progress handling to provide real-time updates during the export process, improving user feedback and experience.

* refactor(web): streamline export capture bridge and update connector styles

- Removed unused loading logic for html2canvas in the export capture bridge, simplifying the code.
- Updated CSS for the onboarding view connector to improve visual clarity and ensure it does not overlap with node numbers.

* refactor(web): remove html2canvas dependency and enhance Figma URL handling

- Removed the html2canvas package from the project, including its references in the lock file and related components.
- Added functionality to manage Figma URLs within the Design System flow, allowing users to add, remove, and validate Figma file links.
- Improved drag-and-drop handling to prevent unintended file navigation when dropping files outside designated areas.
- Updated UI components to accommodate new Figma URL features, enhancing user experience and accessibility.

* refactor(web): unify brand and design system flows

- Merged the brand extraction process into the design system creation workflow, allowing users to start from a brand directly within the design system wizard.
- Updated routing to redirect legacy brand links to the unified design systems tab.
- Enhanced the onboarding experience by removing the separate Brand Kit tab and integrating brand selection into the design system creation process.
- Improved UI components to reflect these changes, ensuring a seamless user experience across the application.

* feat(web): introduce brand enrichment banner and picker modal

- Added a new BrandEnrichmentBanner component to allow users to refine programmatically-extracted design systems with AI by selecting design-system skills.
- Implemented a BrandPickerModal for selecting brands from a searchable gallery, enhancing the design system creation flow.
- Updated ChatPane to conditionally display the enrichment banner for eligible brand projects, improving user engagement.
- Enhanced the design system flow to support the new brand enrichment features, ensuring a seamless experience for users.

* feat(web): enhance BrandPickerModal and DesignSystemAssetDropzone

- Updated the BrandPickerModal to allow scrolling of the entire picker area, improving user experience by creating a unified scrolling surface.
- Added new props to the BrandReferencePicker for action labels and scroll root reference, enhancing flexibility in brand selection.
- Introduced a new DesignKitView component for rendering design kits consistently across different surfaces.
- Enhanced the DesignSystemAssetDropzone to support a wider variety of file types with appropriate previews, improving asset management during design system creation.
- Updated styles for better visual clarity and responsiveness across components.

* feat(web): update Design Systems tab actions and enhance localization

- Changed the button label in the DesignSystemsTab from "Edit" to "Open" for better clarity in user actions.
- Added a new translation key for 'dsManager.openSystem' across multiple languages to support the updated button label.
- Enhanced the FileWorkspace component to ensure the Design Files tab aligns correctly with the Design System tab, improving UI consistency.
- Implemented a new design system editing feature that allows users to fetch and save design system content from DESIGN.md, enhancing the design workflow.

* fix(merge): repair post-merge regressions after origin/main integration

Follow-up fixes on top of the origin/main merge (886f925cd) addressing
regressions the conflict resolution introduced. main's web suite is the
oracle (100% green); resolution principle was main's engine/backend +
HEAD's UI, unioned.

daemon:
- library-sync.ts: correct design-systems import to ./design-systems/index.js
  (design-systems became a directory module on main).
- tests/server-bootstrap-regression: add LIBRARY_DIR to the PathDeps fixture
  (main-added test x HEAD-added LIBRARY_DIR field).

web:
- WorkspaceTabsBar: union — restore main's onboarding Search-popover close
  behaviour + guards, keep HEAD's library/brands nav entries.
- HomeView: restore main's composer sending-state (await onSubmit, widen its
  return type to Promise<boolean>|boolean|void, pass submitting to HomeHero).
- MemorySection.test: take main's version to match main's two-loop memory
  component.
- i18n: restore dropped settings.onboardingRoleMarketing key across types.ts
  and all locales.
- App/BrandsTab/EntryNavRail/router/home-intent: union fixes restoring main
  features dropped during conflict resolution (needs_input handling, etc.).

Validation: pnpm guard + full pnpm typecheck (all 23 packages) green.
Known-red: EntryShell onboarding step 3 intentionally retains HEAD's "build"
step rather than main's brand-extract step; 8 EntryShell.onboarding /
App.onboarding-amr-e2e tests stay red pending that onboarding decision.

* fix(merge): keep HEAD's unified brand flow (revert main's separate Brands tab)

Follow-up to 688544ff7. Per the chosen product direction (brand creation
unified into the design-system create wizard, not a standalone Brands tab),
revert the brand-flow routing/nav that the post-merge repair had restored
from main:

- router.ts: keep HEAD's brand routing (brands folded into design-systems),
  drop main's standalone /brands and /brands/:id view routing.
- EntryNavRail.tsx: drop main's standalone "Brands" nav button.
- runtime/home-intent.ts: drop main's brand "Use in new chat" confirmation
  notice plumbing (tied to the separate Brands flow).

Kept from the repair commit (additive, non-conflicting): App.tsx
loadedActiveProject correctness, composer Sending… state, WorkspaceTabsBar
onboarding popover behaviour, two-loop memory test, restored i18n keys,
brand needs_input STATUS handling, server.ts plugin-route infrastructure.

* feat(library-ui): implement conditional rendering based on LIBRARY_UI_VISIBLE

- Updated router.ts to conditionally render the library view based on the LIBRARY_UI_VISIBLE flag.
- Modified ComposerPlusMenu.tsx, DesignFilesPanel.tsx, and DesignSystemAssetDropzone.tsx to show the "Select from library" button only when LIBRARY_UI_VISIBLE is true.
- Adjusted EntryNavRail.tsx and EntryShell.tsx to include the library navigation button and section conditionally based on the LIBRARY_UI_VISIBLE state.
- Enhanced HomeHero.tsx to allow starting a blank project directly, improving user experience by providing more options for project creation.

This commit introduces a feature toggle for the library UI, allowing for better control over its visibility during development and testing.

* feat(home-hero): implement edge auto-scroll for horizontal overflow

- Introduced `useEdgeAutoScroll` hook to manage auto-scrolling behavior for horizontally overflowing components in HomeHero.
- Updated `PluginPromptPresets` and `RailGroup` components to utilize the new auto-scroll functionality, enhancing accessibility for users without trackpads.
- Added `EdgeScrollZones` component to provide interactive edge zones for scrolling.
- Enhanced CSS styles to support the new scrolling layout and ensure proper positioning of elements.

This commit improves user experience by making overflow content more accessible and easier to navigate.

* feat(design-systems): add project creation from design system and enhance UI components

- Implemented `handleCreateProjectFromDesignSystem` function in `AppInner` to facilitate project creation directly from a selected design system.
- Updated `DesignKitView` to wrap the iframe in a span for better layout control.
- Refactored CSS for `BrandPreviewCard` and `DesignSystemsTab` to improve styling and responsiveness.
- Introduced a new `TemplatePicker` component in `HomeHero` for selecting project-type templates, enhancing user experience.
- Updated various components to support asynchronous handlers for design system actions, improving overall functionality.

This commit enhances the design system integration and user interface, making project creation more intuitive and accessible.

* feat(brand-routes): enhance brand reservation API and add DESIGN.md support

- Updated the POST /api/brands endpoint to accept optional fields: description and designMd, allowing for more flexible brand reservations.
- Modified validation to require either a URL or designMd for brand extraction.
- Introduced a new design-md-input module to handle parsing and validation of DESIGN.md content.
- Enhanced startBrandExtraction function to support processing of DESIGN.md, improving integration with design systems.
- Added utility functions for managing DESIGN.md input and output, streamlining the brand creation process.

This commit improves the brand extraction workflow by integrating DESIGN.md support, making it easier for users to create and manage brands.

* feat(chat-pane, design-kit-view): enhance chat functionality and design preview features

- Added `handleNextStepPromptAction` to `ChatPane` for setting draft prompts, improving user interaction.
- Introduced `nextStepVariant` to differentiate design system projects in `ChatPane`.
- Updated `DesignKitView` to include a button for previewing design kit covers, enhancing user experience.
- Implemented a modal for displaying design kit previews, allowing users to view content in a dedicated space.

These changes improve the chat interface and design kit interactions, making the application more intuitive and user-friendly.

* feat(brand-extraction): enhance DESIGN.md support and testing

- Added a new test case to validate brand extraction from DESIGN.md input without requiring a website.
- Implemented functionality to register brands directly from DESIGN.md, improving the brand creation workflow.
- Updated the `ChatPane` and `NextStepActions` components to handle design system-specific actions for projects, enhancing user experience.
- Enhanced localization files with new carousel hints and project brief options across multiple languages.

These changes streamline the brand extraction process and improve the overall functionality of the design system integration.

* feat(wireframe-examples): add annotated and greybox wireframe examples

- Introduced new wireframe examples for annotated and greybox styles, enhancing design system capabilities.
- Added HTML and JSON files for both wireframe types, providing templates for low-fidelity design mockups.
- Implemented SKILL.md documentation for each wireframe example, detailing usage and design specifications.

These additions improve the design toolkit, offering users more options for creating wireframes in various styles.

* feat(brand-extraction): refine Chrome fallback and enhance error handling

- Updated the Chrome fallback logic in the prefetch pipeline to clarify its purpose and usage as a diagnostic tool.
- Introduced environment variable checks to enable or disable system Chrome usage, improving control over the extraction process.
- Enhanced error messages in the DesignSystemCreationFlow component to provide clearer guidance on required inputs for creating a design system.
- Added regression tests to ensure that prompts do not instruct the agent to invoke a non-existent `brand-extract` skill, preventing potential failures during brand extraction.

These changes improve the robustness of the brand extraction process and enhance user experience by providing clearer instructions and error handling.

* feat(brand-extraction): enhance DESIGN.md input handling and introduce brand ready prompt

- Updated the BrandFromDesignMdInput interface to explicitly define the description property as optional with undefined.
- Enhanced the brand extraction prompts to clarify the inline brand-extract workflow, preventing confusion during the extraction process.
- Added a new BrandReadyPrompt component to notify users when a design system is ready for preview, improving user experience.
- Introduced CSS styles for the BrandReadyPrompt to ensure a visually appealing and user-friendly interface.
- Updated localization files to support new strings related to the brand ready prompt across multiple languages.

These changes improve the clarity and usability of the brand extraction process, providing users with timely feedback and a more intuitive interface.

* feat(brand-extraction): improve design system focus handling and localization updates

- Refactored the handling of browser tabs in the brand extraction tests to ensure proper validation of tab states.
- Enhanced the AppInner component to refresh design systems alongside templates, ensuring users see the latest updates without page reloads.
- Introduced a pending focus state in the DesignSystemsTab to manage design system selection more effectively after brand extraction.
- Added a BrandReadyPrompt in the ProjectView to notify users when a design system is ready for preview, improving user engagement.
- Updated localization files for Chinese (Simplified and Traditional) to reflect changes in terminology related to design systems.

These changes enhance the user experience by providing timely feedback and ensuring that the design system selection process is seamless and intuitive.

* fix(styles): adjust letter-spacing and enhance plus-menu trigger styles

- Set letter-spacing to 0 in design-system-flow.css for improved text clarity.
- Added styles for plus-menu trigger in plus-menu.css, including background, border, and hover effects to enhance user interaction and visual consistency.

These changes refine the design aesthetics and improve the usability of the plus-menu component.

* feat(tests): add design-system focus handoff tests

- Introduced a new test suite for validating the design-system focus handoff functionality.
- Implemented tests to ensure that the focus ID is correctly set, read, and cleared from session storage, preventing user selection hijacking.
- Added checks for scenarios where no focus ID is pending, enhancing test coverage for the design system's behavior.

These tests ensure the reliability of the design-system focus handling, contributing to a more robust user experience.

* feat(export): restrict image format options to PNG and JPEG

- Updated the image format options in the export functionality to only allow PNG and JPEG, removing WebP to prevent silent downgrades.
- Enhanced error handling to provide clear feedback when an unsupported image format is specified.
- Adjusted related documentation and comments to reflect the changes in supported formats across the application.

These changes ensure consistency in image export behavior and improve user experience by providing immediate validation errors for unsupported formats.

* feat(origin-validation): implement zero-config OD Clipper bypass for library requests

- Added a new function `isZeroConfigClipperLibraryRequest` to validate requests from locally-installed browser extensions targeting the `/library/` path.
- Updated the origin validation middleware to utilize this function, allowing unpaired browser extensions to access the `/api/library/ingest` endpoint while blocking other cross-origin requests.
- Enhanced tests to cover the new bypass functionality, ensuring correct behavior for both valid and invalid origins.

These changes improve the integration of browser extensions with the local daemon, enhancing user experience while maintaining security.

* feat(design-systems): add download functionality for design systems

- Implemented a new command `od design-systems download <id>` to allow users to download design systems as a .zip file, including all system files and a generated SKILLS.md usage guide.
- Updated the CLI help documentation to include usage instructions for the new download command.
- Enhanced the design systems API to support the download feature, ensuring only user design systems are accessible while handling errors for non-existent presets.
- Added localization strings for the new download functionality across multiple languages.

These changes enhance the usability of design systems by providing a straightforward method for users to obtain and share their design assets.

* feat(design-systems): enhance design system management and localization

- Introduced new UI components and styles for managing design systems, including buttons for downloading, refreshing, and resetting edits.
- Updated the DesignKitView to support direct actions for DESIGN.md editing, improving user interaction with design systems.
- Enhanced the DesignSystemDetail component to include download functionality and improved state management for design system edits.
- Added localization strings for new features, ensuring consistent user experience across multiple languages.
- Improved error handling and user feedback for design system operations, including download failures.

These changes streamline the design system management process, making it more intuitive and user-friendly while ensuring robust localization support.

* feat(tests): add comprehensive tests for design system archive functionality

- Introduced a new test suite for validating the `buildUserDesignSystemArchive` and `buildDesignSystemSkillsMarkdown` functions.
- Implemented tests to ensure correct packing of design system files, including the generation of a `SKILLS.md` guide and exclusion of internal metadata.
- Added checks for handling non-user IDs and scenarios where a design system already includes its own `SKILLS.md`.
- Enhanced the overall test coverage for design system functionalities, ensuring reliability and correctness in the design system archive process.

These changes improve the robustness of the design system features by ensuring thorough testing of critical functionalities.

* feat(figma-import): enhance CLI output and add Figma import endpoint

- Updated the CLI to conditionally log detailed import information based on the `--json` flag, improving usability for users who prefer JSON output.
- Introduced a new API endpoint for importing Figma files, handling file uploads and validating project existence, with appropriate error responses for missing files or invalid URLs.
- Added a dedicated route for the Figma import functionality, ensuring seamless integration with existing project workflows.

These changes improve the Figma import experience by providing clearer output options and robust error handling, enhancing overall user interaction with the CLI and API.

* feat(design-files): enhance DesignFilesPanel with new actions and styles

- Added new action buttons for opening a browser and creating a design system in the DesignFilesPanel, improving user interaction in the empty state.
- Updated styles for action buttons to enhance visual distinction and usability.
- Enhanced tests to verify the functionality of new actions in the DesignFilesPanel, ensuring they trigger correctly.

These changes improve the user experience by providing additional functionality and clearer visual cues in the design files interface.

* fix(ci): restore new project modal flow

* fix(ci): align design kit and onboarding checks

* fix(ci): sync bake preview workflow action

* fix(ci): include plugin preview helper scripts

* fix(review): harden brand source and preview flows

* fix(ci): stabilize web workspace tests

* fix(review): address latest blocking feedback

* chore(ci): retrigger validation after label update

* chore: re-trigger CI on updated main — needs-validation gate moved to merge_group (#4714)

* refactor(lightbox): implement portal for overlays to resolve z-index issues

- Updated the lightbox component to use React's createPortal for rendering overlays directly to the <body>, ensuring proper z-index stacking.
- Removed session mode toggle from HomeHero and adjusted related styles and tests accordingly.
- Cleaned up CSS by removing unused styles related to session mode toggle.
- Updated tests to reflect changes in the HomeHero component and its interaction with the design router.

* style(home-hero): remove focus halo from template search input

- Updated CSS to eliminate the global input focus outline and box-shadow for the template search field in the HomeHero component.
- Added a test to verify that the template picker search field maintains a clean appearance when focused.

* feat(design-system): add create design CTA and enhance design kit functionality

- Introduced a new `DesignSystemCreateCta` component to facilitate creating new designs from an active design system, enhancing user experience in the chat interface.
- Updated `ChatPane` to include the new CTA, allowing users to create designs directly from the chat.
- Enhanced `DesignKitView` with sticky header functionality for better accessibility while scrolling.
- Added new CSS styles for the `DesignSystemCreateCta` component to ensure a visually appealing and consistent design.
- Updated internationalization files to include new strings for the design system creation feature.

* feat(upload): enhance file upload handling and error recovery

- Introduced `sanitizePath` to preserve directory structures during file uploads, preventing issues with subdirectory paths.
- Updated `DesignKitView` and related components to utilize the new `sanitizePath` function for improved file name resolution.
- Added `KitErrorBoundary` component to gracefully handle rendering errors in the design kit, providing a user-friendly fallback.
- Implemented internationalization updates for new error messages and action confirmations related to uploads and error handling.
- Enhanced CSS styles for better visual feedback during error states and improved user experience.

* feat(design-kit): add keyboard shortcuts hint and enhance key handling

- Introduced a new keyboard shortcuts hint in the DesignKitView, providing users with quick access to essential actions (E edit, C copy, U upload, R refresh, ⌫ delete logo).
- Implemented a keydown event handler to manage keyboard shortcuts contextually within the design kit, improving user interaction and accessibility.
- Updated CSS for the shortcuts hint to ensure it remains low-contrast until hovered, enhancing the UI experience.
- Added internationalization support for new shortcut labels and hints across multiple languages.
- Adjusted DesignSystemsTab to prefer user logos for their systems, improving visual consistency.

* feat(design-system): introduce DesignSystemExtractionPanel and enhance design system interactions

- Added the `DesignSystemExtractionPanel` component to facilitate user interactions during design system extraction, providing a synthesized conversation view and next steps.
- Updated `ChatPane` to render the new extraction panel when a design system is active, enhancing user guidance.
- Introduced a new utility function `designSystemExtractionSource` to derive human-readable labels for design system sources.
- Enhanced internationalization support with new strings for extraction-related actions and prompts across multiple languages.
- Updated various components and tests to reflect changes in terminology and functionality, improving overall user experience.

* feat(project): add project deletion functionality and enhance design system interactions

- Introduced `onDeleteProject` prop in `ProjectView` to handle project deletion, improving project management capabilities.
- Updated `AppInner` to include the new delete project handler, enhancing user experience in project interactions.
- Enhanced `DesignKitView` and `DesignSystemsTab` with loading states and improved visual feedback during design system resolution.
- Removed deprecated `DesignSystemCreateCta` component and associated styles, streamlining the codebase.
- Updated internationalization files to reflect changes in project management terminology and actions.

* feat(design-kit): enhance internationalization and user feedback in DesignKitView

- Updated various labels and error messages in the DesignKitView to utilize internationalization functions, improving accessibility and user experience.
- Enhanced color input validation messages and added confirmation prompts for design system deletions in DesignSystemsTab and FileWorkspace.
- Introduced new props for handling design system project deletions, streamlining project management.
- Updated internationalization files to reflect new strings and translations for improved user guidance across multiple languages.

* refactor(design-kit): remove keyboard shortcuts hint and streamline header menu

- Eliminated the keyboard shortcuts hint from the DesignKitView, simplifying the header menu.
- Updated the sticky-header overflow menu to exclude upload, full-system preview, and shortcut help actions, focusing on essential project operations.
- Adjusted related tests to reflect the removal of the shortcuts hint and ensure accurate menu item visibility.

* feat(brand-routes): add extract-from-html endpoint for brand extraction

- Introduced a new POST endpoint `/api/brands/:id/extract-from-html` to re-run brand extraction using HTML rendered from the in-app browser after clearing anti-bot walls.
- Implemented error handling for missing HTML and brand not found scenarios.
- Enhanced the `extractBrandFromHtml` function to process the provided HTML and optional CSS, integrating it into the existing brand extraction workflow.
- Updated `prefetch` functionality to support extraction from pre-rendered HTML, improving the overall brand data retrieval process.

* chore(nix): refresh pnpm deps hash

* feat(brand-cli): add extract-from-html command for brand extraction

- Introduced a new CLI command `od brand extract-from-html` to facilitate brand extraction from pre-captured rendered HTML, allowing users to bypass anti-bot walls.
- Enhanced the command to accept optional CSS and base URL parameters, improving flexibility in extraction scenarios.
- Implemented error handling for missing HTML input and invalid brand IDs, ensuring robust user feedback.
- Updated the `BRAND_USAGE` documentation to reflect the new command and its usage details.
- Adjusted server configuration to accommodate larger payloads for the new extraction endpoint.

* feat(design-system): enhance design system extraction and browser tools

- Added a new script to collect CSS styles from rendered pages, improving brand extraction capabilities by capturing computed styles from cross-origin stylesheets.
- Removed the `DesignSystemExtractionPanel` component and its associated styles, streamlining the codebase.
- Updated `ProjectView` and `FileWorkspace` components to enhance design system interactions and improve user experience.
- Introduced new internationalization strings for design system phases and actions, ensuring better user guidance across multiple languages.

* feat(brand-assist): implement browser assist for brand extraction

- Added support for a client-side confirmation mechanism for the brand-browser-assist od-card, allowing users to extract brand information from the unblocked browser DOM.
- Enhanced the `ProjectView`, `ChatPane`, and `AssistantMessage` components to handle the new assist functionality, improving user interaction during brand extraction.
- Introduced new internationalization strings for browser assist prompts and messages, ensuring clarity and guidance across multiple languages.
- Updated the `useBrandReadyPrompt` hook to manage the state of the browser assist, providing a seamless user experience when dealing with anti-bot walls.

* feat(brand-prompt): enhance BrandReadyPrompt with refinement options

- Updated the BrandReadyPrompt component to include options for AI optimization and manual editing, allowing users to refine extracted brand systems.
- Added a new refinement nudge to inform users that automatic extraction may miss details, improving user guidance.
- Adjusted styles for the prompt and dismiss button for better alignment and visual consistency.
- Introduced new internationalization strings for the refinement features, ensuring clarity across multiple languages.
- Removed deprecated PPTX export functionality from the FileViewer component, streamlining the export options.

* refactor(export): remove PPTX export functionality and streamline export options

- Eliminated PPTX export support across various components, including CLI, desktop, and web, to simplify export formats.
- Updated documentation and help messages to reflect the removal of PPTX, ensuring clarity for users.
- Adjusted export-related types and constants to focus on PDF and image formats only, enhancing code maintainability.
- Improved user experience by refining export options and related UI elements.

* refactor(export): remove PPTX references and update export functionality

- Removed all instances of PPTX export functionality from the codebase, including related dependencies and comments.
- Updated export options to focus solely on PDF and image formats, enhancing clarity and maintainability.
- Adjusted UI components and tests to reflect the removal of PPTX, ensuring a streamlined user experience.
- Improved internationalization strings and documentation to align with the new export capabilities.

* chore(nix): refresh pnpm deps hash

* fix(onboarding): preserve selected runtime

* fix(brand): localize generated kit copy

* fix(onboarding): align first-run flow with main

* fix(nav): use palette icon for design systems

* fix(analytics): use design system onboarding step

* fix(ui): remove design system guide toggle

* fix(ui): position design system ready prompt

* fix(ui): space plugin task notice

* fix(web): restore home ask mode and design kit preview

* test(e2e): align onboarding visual capture

* test(e2e): align amr onboarding checks

* fix(brand): remove blocked reference brands

* feat(onboarding): show profile choices as chips

* fix(home): prefer design system cover art in recents

* test(e2e): select onboarding profile chips

* feat(brand-extraction): implement programmatic extraction transcript and UI enhancements for design systems

* feat(brand-extraction): enhance programmatic extraction with transcript agent support and UI improvements

* feat(brand-extraction): add transcript agent resolution and improve message handling in brand extraction

* fix(design-systems): stabilize loading state coverage

* test(e2e): align design system detail visual

* fix(brand-extraction): backfill programmatic transcripts

* fix(web): refresh ready brand design systems

* fix(brands): stabilize extraction handoff and seed colors

* fix(brands): return extraction transcript immediately

* fix(web): open new project modal from entry rail

* fix(editing): expose content edits for plain targets

* feat(file-viewer): implement manual edit draft dirty state tracking and reset logic

* feat(design-system): enhance project creation flow with conversation ID handling

* feat(brands): implement light theme handling for color extraction and seed generation

* feat(brands): add finalizeBrandProject function for brand project completion

* feat(file-workspace): add designSystemBrandId prop and update DesignSystemProjectPanel to use it

* Fix manual editing for brand kits

* fix(design-system): wait for project refreshes

* fix(web): open new project modal from rail

* fix(web): restore home ask mode toggle

* fix(web): sync brand color edits with seeds

* fix(web): stabilize design system workspace tests

* test(tools-pack): relax Windows resource cache timeout

* chore(pr): retrigger review after validation

* fix(web): surface design kit action progress

* fix(web): clarify brand next-step actions

* fix(web): cancel programmatic brand extraction

* fix(web): add design systems tab action feedback

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: xne998808-ai <xne998808@gmail.com>
Co-authored-by: PerishCode <perishcode@gmail.com>
Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>
Co-authored-by: lefarcen <935902669@qq.com>
Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-06-25 03:56:14 +00:00
michaeltaxman911-hue 86824a30d8 feat(web): unify task-failure error card across all failure states (#4656)
* feat(web): unify task-failure error card across all failure states

Collapse the three divergent failure UIs (gray .msg.error card, red
.status-pill.is-error pill, accent .amr-card) into one neutral run-error
card with a consistent four-part structure: ① error-type title, ② detail
copy, ③ 1–2 fix CTAs anchored bottom-right, ④ collapsible error-source
(code-line styled, one line collapsed, expand/copy on the source row).

- amr-guidance: add titleKey mapping (code → title i18n); split "sign-in
  required" into AMR (inline AmrLoginPill, single CTA, no promotion) vs
  non-AMR ({agent} copy, retry + use-official-agent) per the PRD.
- ChatPane: rebuild the error card JSX into the unified shell, preserving
  every recovery handler (authorize/recharge/resume/retry/BYOK); interpolate
  {agent} via t(key, {agent}) + agentDisplayName.
- AmrLoginPill: drop the redundant device-code chip (the activation URL
  already carries the code); reword the activation hint.
- i18n: add run-error title/source/sign-in keys across all 19 locales.
- tests: update amr-guidance + AmrLoginPill specs for the new behavior.

* test(e2e): align run error assertions with unified card

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: Mason <jinmeihong0201@gmail.com>
2026-06-23 04:08:24 +00:00
YOMXXX 20c61f7732 fix(web): gate PDF print-ready handshake on a usable content size (#4458) (#4545)
* fix(web): gate PDF print-ready handshake on a usable content size (#4458)

One blank-PDF path in #4458: the in-iframe print-ready handshake reported the
artifact's content size after a fixed two animation frames, which can fire
before a heavier artifact has finished layout (size still 0). With no usable
__odPrintSize, the desktop bridge's inferPageSize falls back to measuring the
wrapper viewport — which, per its own docs, blanks artifacts whose visible
content sits below the fold.

- Add reportPrintSizeWhenStable(): poll animation frames until the measured
  size is usable (positive, finite), then report it; bounded so a genuinely
  empty artifact reports best-effort instead of hanging the handshake.
- Add isUsablePrintSize() and use it in the parent readiness cache's size
  validation. Both are injected via toString() and unit-tested directly
  (real-logic behavior assertions, not just string-presence).

Scoped to this one root-cause path. A separate blank-page mode where the
reported size is valid but printToPDF captures before the iframe composites
needs a real-Electron repro to confirm and is left for follow-up.

* test(web): add eval-based runtime behavior regression for the print-ready cache (#4458)

#4458 notes the existing coverage only proves the print-ready scripts are
injected, never that they behave. Extract the real parent-cache <script> from
a live export, run it against a minimal fake window, and drive it with
postMessage events to assert the runtime size gate: a usable size is cached
for inferPageSize(), while a zero/non-finite size or a wrong-nonce message is
rejected so it cannot blank or poison the PDF page.
2026-06-22 09:16:48 +00:00
Tom Huang d19c53d274 Agent-driven brand extraction + two-loop memory overhaul (#4260)
* Implement brand management routes and CLI support

- Added `brand-routes.ts` to handle HTTP endpoints for brand operations: listing, extracting, retrieving details, deleting, and serving logos.
- Introduced `brands-cli-help.ts` for CLI commands related to brand management, including usage instructions for listing, creating, retrieving, and deleting brands.
- Updated `cli.ts` to integrate brand commands into the existing CLI structure, allowing headless management of brands via the command line.
- Created supporting files for brand metadata handling, including `design-md.ts` for rendering brand information in markdown format and `index.ts` for the brand engine API.
- Implemented `prefetch.ts` to fetch and process brand material from specified URLs, ensuring a streamlined extraction process.
- Enhanced server setup in `server.ts` to register brand routes and manage brand-related data effectively.

This commit establishes a comprehensive framework for managing brands within the application, facilitating both HTTP and CLI interactions.

* Enhance memory management and onboarding experience

- Introduced canonical profile labels to ensure consistent handling of user input in profile forms, preventing duplicate entries.
- Updated the `parseProfileBody` and `captureProfileFromForm` functions to utilize the new canonical label matching.
- Added a memory callout section in the onboarding view to highlight the benefits of memory usage, including personalized responses and reduced setup questions.
- Implemented new UI elements in the onboarding view to improve user engagement with memory features.
- Expanded i18n support for new onboarding messages related to memory benefits across multiple languages.

* Refactor onboarding flow and enhance design system integration

- Updated the onboarding process to include a new brand extraction step, replacing the previous newsletter step.
- Adjusted the tracking logic to reflect the new onboarding steps, ensuring accurate analytics for user progress.
- Improved the UI for the onboarding view, including new input fields for email collection during the brand extraction phase.
- Refined the EntryShell component to remove outdated comments and clarify the onboarding renderer's purpose.
- Enhanced CSS styles for the onboarding steps to improve layout and user experience.
- Updated internationalization strings across multiple languages to reflect changes in the onboarding flow and brand extraction messaging.

* Add brand management features and enhance font handling

- Introduced new modules for managing brand assets, including `chrome.ts` for headless Chrome operations and `fonts.ts` for self-hosting web fonts.
- Implemented `prefetch.ts` to streamline the brand material extraction process, allowing for efficient harvesting of colors, fonts, and logos.
- Enhanced the brand system with new schema definitions in `schema.ts` to support brand color and font management.
- Developed the `engine` module to integrate brand building and rendering processes, including token derivation and artifact generation.
- Improved the overall structure and organization of brand-related files for better maintainability and scalability.

* Enhance brand extraction and project management features

- Updated `brand-routes.ts` to include new dependencies for project management, allowing for the registration of brand-related projects.
- Modified the `extractBrand` function to support project ID and system files, improving the brand extraction process.
- Enhanced the CLI commands in `cli.ts` to handle project IDs during brand creation, enabling better tracking of brand projects.
- Updated the server setup in `server.ts` to register new project-related routes.
- Improved the UI components to display project information associated with brands, including buttons for opening projects in the `BrandDetailView` and `BrandsTab`.
- Added new metadata fields in the contracts to support project tracking and management for brands.

This commit establishes a more robust framework for managing brand projects, enhancing both backend and frontend functionalities.

* Enhance onboarding profile management and memory persistence

- Added new canonical profile labels for 'Organization size', 'Use cases', and 'Discovery source' to improve user input consistency.
- Introduced `OnboardingProfileState` type to manage onboarding profile data more effectively.
- Implemented functions to build and persist the onboarding profile body to memory, ensuring user selections are saved accurately.
- Updated the `OnboardingView` component to handle profile persistence during navigation and submission steps.
- Enhanced tests to verify that user selections are correctly persisted to the memory profile.

This commit improves the onboarding experience by ensuring that user inputs are consistently captured and stored, enhancing overall user engagement with the application.

* Reflow brand extraction into an agent-driven, live flow

Replace the deterministic SSE prefetch/preview/system pipeline with an
agent-driven extraction: POST /api/brands now reserves the brand and stands
up a backing project with the target site open in an in-app browser tab plus
a seeded prompt, so the agent measures, synthesizes brand.json incrementally,
and the user can clear anti-bot walls by hand. New /preview and /finalize
routes let the agent render the kit page live and register the resulting
user:<id> design system, so extracted brand facts persist as a structured,
reusable brand kit instead of a one-shot deterministic guess.

Adds the brand-extract skill (SKILL.md + brand-kit.html template), kit-render
engine, brand-extraction-engine tests, brand project covers in the Designs
tab, onboarding extract handoff, and the matching od brand extract/preview/
finalize CLI subcommands and contract updates.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Sediment finalized brands into structured memory

Reflow a finalized brand into the memory store (brandToMemoryEntries +
reflowBrandToMemory) so future chats can ground vague requests in the
brand's palette, type, voice and rules. finalizeBrand now wires through
the runtime dataDir and best-effort persists the brand, MemoryChangeEvent
gains a 'brand' source, and the brand kit render hardens its inline JSON
escaping. Adds brand.previewEmpty / brand.viewDetails across all locales.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Implement logo fallback and imagery support in brand extraction

- Introduced a deterministic logo fallback mechanism to ensure that brand extraction processes can retrieve and save site logos, even when the agent fails to do so.
- Enhanced the `startBrandExtraction` and `finalizeBrand` functions to utilize the new logo fallback, allowing for better handling of logo assets.
- Added support for imagery samples in brand validation, enabling the inclusion of representative images in the brand kit.
- Updated the brand kit rendering to include self-hosted fonts and imagery, improving the overall presentation of brand assets.

This commit strengthens the brand extraction workflow by ensuring that logos and imagery are reliably captured and displayed, enhancing the user experience in brand management.

* Enhance memory management with rule proposal and verification features

- Introduced new functionality for distilling annotations into rule proposals, allowing users to suggest rules based on in-canvas annotations through the `od memory rule suggest` command.
- Implemented a verification system that programmatically enforces compliance with active rules during artifact generation, ensuring that all active rules are covered in the self-verify scorecard.
- Added endpoints for managing verification outcomes, including listing, removing, and clearing verification records, enhancing the transparency of the verification process.
- Updated the memory management system to support the retrieval of active rule entries, ensuring that only linked rules are considered during verification.
- Enhanced tests for both rule proposal generation and verification processes to ensure reliability and correctness.

This commit strengthens the memory management capabilities by integrating rule proposals and verification, improving the overall user experience in managing design rules and ensuring compliance.

* Distill review annotations into memory and enforce self-verify scorecard

Add distillAnnotationsToMemory to mine inline preview comments/highlights/
marks into durable feedback + rule memory via a dedicated distiller prompt,
threaded through the existing extract pipeline with an 'annotation' change
source. Tighten the self-verify prompt (daemon + contracts) to state the
daemon programmatically checks the scorecard, so a missing or uncovered
scorecard on an artifact turn is an enforcement failure. Cover the rule
suggest and verification-history routes with tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply brand design system through web config on "Use in new chat"

Thread onApplyDesignSystem from the entry shell into BrandsTab so the brand's
registered design system is applied via the web config channel instead of a
bare daemon PATCH that left the Home composer stale. Add a transient
home-intent latch + event so the Brands tab can request the Prototype chip on
the already-mounted HomeView, which consumes it once the plugin catalog loads.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Wire annotation distillation into background memory extraction

Add a background distill pass that mines inline review annotations
(comments / highlights / drawn marks) from a turn into durable memory
alongside the general LLM extraction, surface an `annotation` memory
toast source in the web UI, and cover the flow with a unit test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix brand design system not applying to composer on "Use in new chat"

Selecting a brand's "Use in new chat" applies the brand's design system as
the default and fires the Prototype chip intent in the same synchronous click
handler. HomeView consumed that intent inside the event listener, so `pickChip`
ran before React committed the config change and seeded the composer's
design-system field from the stale (empty) default — the composer showed
"No design system" instead of the brand until a reload.

Split the intent handling: the listener now only bumps a tick, and a separate
effect consumes the chip after the re-render lands, so the seeded design system
reflects the freshly-applied brand. Add the previously-untracked home-intent
latch test coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): rework Brands into Brand Kit and add Home create entry

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(brands): harvest real cover/hero images for the Images module

The brand kit's Images gallery only populated when the extraction agent
remembered to save imagery — so a forgetful or bot-blocked agent (and the
pre-imagery "Open Design" brand) left it empty. Add a deterministic,
server-side imagery fallback (imagery-fallback.ts), mirroring the logo
fallback: it parses og:image/twitter:image, large <img> (highest-res
srcset/<picture>), <link rel=preload as=image>, and CSS background-image
hero blocks, fetches candidates with browser-shaped headers, decodes
PNG/GIF/JPEG/WebP dimensions to keep only big representative images
(dropping icons/sprites/logos/tracking pixels), dedupes by content hash,
and saves up to 8 of the largest into imagery/ with labeled samples.

finalizeBrand runs it as a timeout-bounded, failure-tolerant safety net
(injectable so tests stay offline) when the agent captured too few
samples, first adopting any on-disk images. The extraction prompt and
brand-extract SKILL now explicitly direct the agent to harvest the site's
large/cover/hero images, filtered by rendered size.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(qa): implement deck layout validation and safety checks

Add a new QA module for validating the layout of generated brand decks to ensure robustness against clipping and truncation issues. The `analyseDeckLayout` function checks for critical layout invariants, including the presence of `.slide` sections, correct container types, and necessary runtime layers. Introduce `assertDeckLayoutSafe` to enforce these checks during brand system rebuilds, preventing the deployment of decks that fail validation. Additionally, create comprehensive tests to verify the functionality of the new layout validation features.

* fix(brands): apply deck shrink-to-fit synchronously so slides never clip

The no-clip runtime scheduled its fit pass through requestAnimationFrame,
whose callbacks are throttled while the deck is offscreen or occluded. A
slide could therefore stay unscaled — and clip its content — until first
paint. Fit synchronously on resize/load/fonts-ready with a trailing
setTimeout settle pass for late reflow, removing the rAF dependency.

Verified at the previously-broken 1024x620 viewport: container-type:size,
zero truncations, runtime auto-applies scale (Problem 0.71, ASK 0.87,
Product 0.97, Competition 0.97) and frame clip count drops to 0.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): let New Brand modal embed scrollable brand reference picker

Add a fillHeight mode to BrandReferencePicker so the heading, quick-pick
row and controls stay pinned while only the gallery scrolls inside a
bounded-height parent. Wire it into NewBrandModal with a stable, spacious
dialog and refresh the related newBrand/brandPicker copy across all 18
locales.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(brands): enhance brand extraction with deterministic seed harvesting

Introduce a new `seed-fallback` module to provide a server-side deterministic palette and typography seed during brand extraction. This ensures that the brand kit's initial display includes a harvested logo, an approximate color palette, and font families, improving the user experience by reducing the all-skeleton appearance during the first paint. Update the `startBrandExtraction` function to utilize this new module, allowing for a more seamless and visually appealing brand extraction process.

Additionally, enhance the `BrandReferencePicker` component to reflect loading states and errors during brand extraction, ensuring users receive immediate feedback on their actions. Update related tests to verify the idempotency of the `finalizeBrand` function, ensuring that re-finalizing a brand correctly reuses the existing design system without duplication.

* feat(brand-extract): enhance BrandReferencePicker and localization updates

Updated the BrandReferencePicker component to reflect loading states and errors during brand extraction, improving user feedback. Added a new localization key for the brand extraction process and updated existing translations in English, Simplified Chinese, and Traditional Chinese to enhance clarity and user experience. Additionally, introduced new styles for better interaction with brand assets in the brand kit template.

* feat(brands): wire in-page lightbox/masonry/asset preview + refine seed

Brand-kit preview improvements for the live extraction kit:
- brand-kit.html: add in-page overlay system (sandboxed iframe has no
  top-nav) — clickable image lightbox with prev/next, a "view all"
  masonry modal, and a full-page asset preview modal that loads
  system/artifacts/<kind>.html in an iframe. Defer auto-reload while an
  overlay is open so it never yanks the modal out mid-interaction.
- seed-fallback.ts: prefer vivid mid-luminance hues for the seeded
  accent/accent-secondary, and drop icon/symbol faces (Remix Icon etc.)
  from the typography seed so specimens never render glyph soup.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(brands): wire in-page lightbox/masonry/asset preview + refine seed

Brand-kit preview improvements for the live extraction kit:
- brand-kit.html: add in-page overlay system (sandboxed iframe has no
  top-nav) — clickable image lightbox with prev/next, a "view all"
  masonry modal, and a full-page asset preview modal that loads
  system/artifacts/<kind>.html in an iframe. Defer auto-reload while an
  overlay is open so it never yanks the modal out mid-interaction.
- seed-fallback.ts: prefer vivid mid-luminance hues for the seeded
  accent/accent-secondary, and drop icon/symbol faces (Remix Icon etc.)
  from the typography seed so specimens never render glyph soup.

Co-authored-by: Cursor <cursoragent@cursor.com>

* i18n(web): add brandPicker.opening across remaining locales + picker test

Completes the brand-reference picker i18n key that was committed only for
en/zh-CN/zh-TW, so every locale satisfies the typed Dict, and lands the
BrandReferencePicker extraction-feedback test left untracked by the
concurrent worker.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(EntryShell): enhance AMR cloud card visibility post-detection

Updated the EntryShell component to ensure the AMR cloud card remains visible after detection settles, even when the AMR runtime is unavailable. This change prevents the card from disappearing and allows it to degrade gracefully to fallback content and sign-in flow. Additionally, added tests to verify the new behavior, ensuring a better user experience during onboarding.

* feat(brands): programmatic-first design system extraction + rename

Make brand extraction two-phase so a usable design system is ready the
moment the user enters a URL — the instant "aha" — instead of waiting on
the AI agent:

1. PROGRAMMATIC-FIRST (synchronous): startBrandExtraction now harvests the
   site deterministically (logo, palette, typography, one-line description,
   cover imagery, source URL) via prefetchBrand, synthesizes a valid design
   system with brandFromMaterial (no LLM), and finalizes + registers it
   before returning. finalizeBrand is refactored into a reusable
   finalizeBrandCore shared by both the programmatic path and the agent path.
2. ASYNC AI ENRICHMENT: the seeded agent prompt is reframed to enrich the
   already-usable design system and re-finalize in place (same user:<id>),
   updating every artifact/template.

Bounded + best-effort: a blocked/unreachable origin skips phase 1 and stays
`extracting` for the agent to drive. Gated on userDesignSystemsRoot so the
legacy agent-only path stays intact for tests.

Also rename the user-facing "Brand Kit" surface to "Design System" across
en + zh-CN strings, project names, and the enrichment prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(brands): restore design-systems nav entry + reconcile BrandsTab on re-activation

Address review feedback on PR #4260:

1. EntryNavRail dropped the only control that reached view==='design-systems'
   when Brands replaced it in the rail, leaving the still-rendered/routed
   design-system manager deep-link only (the entry-nav-design-systems e2e
   specs assert this). Restore a reachable rail entry (blocks icon, existing
   navDesignSystems key) alongside Brands.

2. BrandsTab only fetched once on mount, but EntryShell keeps sub-views
   mounted and toggles visibility, so a brand finishing extraction in its
   backing project never reconciled until a full reload. Refresh whenever the
   Brands view becomes active again, and poll while any brand is extracting
   (torn down once settled / when hidden).

Red spec: tests/components/BrandsTab.refresh.test.tsx (fails pre-fix:
fetchBrands called once, not twice).

* fix(web): surface missing backing projects

* fix(web): re-enable brand actions after use

* fix(daemon): serve brand logos from data roots

* fix(brands): reconcile failed extractions

* fix(brands): non-blocking extraction start + lifecycle-accurate status (#4470)

Acceptance fixes for the Brand Kit surface (PR #4260, even-ferry),
addressing five reported issues:

- Perf: `startBrandExtraction` no longer blocks the POST /api/brands
  response on the synchronous programmatic harvest+finalize. It now waits
  only a short budget (~1.2s) so fast origins still land on a ready,
  applyable design system (the instant "aha"), while slow/blocked origins
  return immediately on the "Extracting…" skeleton and finalize in the
  background. Measured: example.com POST dropped from a ~19s block to
  ~1.2s, then finalized to `ready` in the background.

- Brand Kit stays stuck on "Extracting…" after a manual stop: the backing
  run's canceled/failed status now reconciles the brand to `failed` with a
  reason (this extends the existing reconcile; adds a regression spec).

- Brand Kit ignores a backing run that is awaiting user input
  (anti-bot wall / question form): new reversible `needs_input` brand
  status, derived on read (never persisted) so it flips back once the user
  answers. Surfaced as a "Needs input" badge + hint in the card and a
  distinct status dot in the list; the list keeps polling while paused.

- Home → Recent projects showed brand-extraction projects as
  "Not started": /api/projects now falls back to the brand lifecycle for
  brand projects whose run-derived status is `not_started`
  (extracting→running, needs_input→awaiting_input, ready→succeeded,
  failed→failed), so a live extraction is never shown as not started.

- "Use in new chat" produced no visible result: it now carries a
  confirmation notice through the Home intent channel so Home shows a
  "Using <brand>" toast, making the navigate+apply verifiable (the
  composer already preselects the brand's design system).

Tests: daemon brand-routes (canceled→failed, needs_input transient) and
brand-extraction-engine (fast return + background finalize) specs; web
BrandPreviewCard specs (needs_input badge/hint, Use-in-new-chat notice).
#4 validated end-to-end against a real daemon. guard + typecheck green.

* [codex] Fix brand extraction incomplete states (#4486)

* fix brand extraction incomplete states

* fix web memory tests for advanced modal

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix onboarding P0 brand-step navigation

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix daemon brand extraction test timeout

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix brand extraction fallback prompt state

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
Co-authored-by: Looper <looper@noreply.github.com>

* [codex] Fix brand extraction terminal status reconciliation (#4532)

* Fix brand extraction terminal status reconciliation

* Keep prefetched brand pages previewable

* fix(brands): preserve ready extraction status

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix(brands): persist terminal extraction signal

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix(brands): clear stale terminal retry markers

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
Co-authored-by: Looper <looper@noreply.github.com>

* Fix brand kit modal intent handling (#4528)

Co-authored-by: Siri-Ray <2667192167@qq.com>

* fix(web): let design system contents scroll in narrow review layout (#4526)

Co-authored-by: Siri-Ray <2667192167@qq.com>

* [codex] Fix Brand Kit preview lifecycle guards (#4527)

* Fix brand kit preview lifecycle guards

* fix(brands): keep detail selected on delete failure

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix(brands): handle delete HTTP failures

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
Co-authored-by: Looper <looper@noreply.github.com>

* ci: retrigger pull_request checks

* test(e2e): stabilize entry settings shard

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: PerishCode <perishcode@gmail.com>
Co-authored-by: lefarcen <935902669@qq.com>
Co-authored-by: PerishFire <39043006+PerishCode@users.noreply.github.com>
Co-authored-by: Siri-Ray <2667192167@qq.com>
Co-authored-by: Looper <looper@noreply.github.com>
2026-06-22 08:34:05 +00:00
Christopher Beaulieu 7cdd399442 fix(web,daemon): sanitize preview document title so printed PDF filenames are Teams-safe (#4278)
* fix(web): sanitize preview document title so printed PDF filenames are Teams-safe (#3918)

* fix(web,daemon): harden invoice title sanitization for URL-load path (#3918)

Four defects in the prior commit (cc29381ad) fixed:

1. [HIGH] URL-load path never sanitized. The srcDoc sanitizer only ran on the
   buildSrcdoc path. Static HTML previews (e.g. design-templates/invoice/
   example.html) render via the URL-load iframe because shouldUrlLoadHtmlPreview
   returns true for script-free artifacts. The URL-load iframe uses
   sandbox="allow-scripts allow-downloads" without allow-same-origin, so the
   host cannot access contentDocument.title after load. Fix: sanitize the
   <title> in the daemon's /api/projects/:id/raw/ handler, which already
   transforms HTML responses before serving them to the iframe.

2. [MED] decodeHtmlEntitiesForTitle crashed on out-of-range numeric entities.
   String.fromCodePoint throws RangeError for values > 0x10FFFF (e.g.
   &#9999999999;), breaking buildSrcdoc for all previews. Fix: safeFromCodePoint
   returns U+FFFD for out-of-range code points instead of throwing.

3. [MED] Named non-ASCII entities left orphaned fragments. A title like
   Fran&ccedil;ois would strip & first, leaving literal "ccedil;" in the
   filename. Fix: decode a common Latin-1 / punctuation named-entity set before
   sanitizing, so &ccedil; → ç → sanitized correctly.

4. [LOW] Title regex could match <title> inside HTML comments or <script>
   blocks. Fix: replaced the blind regex with a scan that skips comment
   (<!-- ... -->) and script/style block contexts before locating the real
   <title> start offset.

Both srcDoc and URL-load paths now produce Teams-safe titles. Six new regression
tests added; all pre-existing tests continue to pass.

* test(daemon): cover URL-load title sanitization (#3918)

Export daemonSanitizeTitleInDoc from project-routes.ts and add
project-routes-title-sanitize.test.ts covering:

- Teams-disallowed chars in a typical invoice title (Invoice #INV/2025:Acme & Co)
- Full disallowed-char set replacement
- Out-of-range numeric entities (&#9999999999; and &#x110000;) do not throw
- Named non-ASCII entities (&ccedil;, &eacute;) leave no orphaned fragments
- <title> inside an HTML comment is not mistaken for the real head title
- <title> inside a <script> block is not mistaken for the real head title
- Body content outside <title> is unchanged

* fix(web,daemon): strip ~$ prefix after trim so whitespace can't leak a Teams-unsafe title (#3918)

The ^~$ anchor in replace(/^~\$/, '') is position-0 anchored. When the
title had leading whitespace (e.g. "  ~$Invoice #1"), the anchor missed,
and after the later .trim() the result still started with ~$ — Teams-unsafe.

Fix: trim first, then loop-strip every leading ~$ prefix (handles doubled
prefixes like "~$~$Doc"), re-trim after each strip, then replace disallowed
chars, then final trim. Applied identically to sanitizePreviewTitle
(apps/web/src/runtime/srcdoc.ts) and daemonSanitizePreviewTitle
(apps/daemon/src/project-routes.ts).

Regression tests added to both test files covering:
- "  ~$Invoice #1" (whitespace before prefix)
- "~$ Invoice" (space after prefix)
- "~$~$Doc" (doubled prefix)
2026-06-21 15:50:00 +00:00
nettee 08668426c7 fix(web): wait for print-ready content before browser PDF export (#4329)
* fix(web): wait for print-ready content before browser PDF export (#3872)

Browser fallback PDF export now reuses the injected print-ready cache so the popup waits for fonts, images, CSS image URLs, and stable layout before calling window.print().\n\nAdd coverage for both sandboxed and trusted browser export wrappers so the fallback path cannot regress to the old fixed-delay behavior.\n\nGenerated-By: looper 0.9.9 (runner=worker, agent=codex)

* fix(web): restore fast PDF fallback when print handshake is blocked

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

* fix(web): force lazy print images eager before PDF wait

Generated-By: looper 0.9.9 (runner=fixer, agent=codex)
2026-06-17 11:35:38 +08:00
free666799 eb749841b4 fix(home): surface community slide decks on Home with letterbox-free 16:9 previews (#4350)
* fix(plugins): surface slides decks on Home instead of marketplace sources

The 23 community slides/deck plugins from #4127 were registered as a
restricted community marketplace source, so they showed up as installable
"available sources" cards (受限 + Install) instead of on Home. Relocate them
to plugins/_official/examples so they ship bundled — appearing in the Home
Community gallery and the deck example-prompt rail, exactly like the existing
zhangzara / swiss / guizang decks — and drop their community-registry entries.

- Move 23 plugins/community/<slug> → plugins/_official/examples/<slug>
  (upstream LICENSE / author / homepage / "Based on" attribution preserved)
- Rename manifest name community-<slug> → example-<slug> (bundled convention)
- Remove the 23 entries from plugins/registry/community/open-design-marketplace.json
  (the 3 pre-existing community entries stay)
- HomeHero: uncap the deck example-prompt rail (other chips keep the curated 18)
  so every bundled slide template is reachable straight from "All"
- curatedPriority: pin the 23 slides to the front of both the deck example rail
  and the Community Slides shelf

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

* fix(home): drop provenance tail from deck seeds & fix deck preview letterbox

Two polish fixes for the slide/deck plugins now surfaced on Home:

1. Example-prompt seed: strip a trailing source-attribution sentence
   ("…。移植自 foo/bar 的 baz 模板。" / "Based on …") from the composer seed so
   the prompt doesn't end on provenance boilerplate. Conservative — only fires
   when the final sentence opens with an attribution marker and real text
   precedes it; attribution woven mid-description (followed by a use-case
   sentence) and the gallery/detail descriptions are left intact. Latin '.'
   only ends a sentence at whitespace/EOL so "STYLE_PRESETS.md" / "github.com/…"
   don't mis-split.

2. Deck gallery preview: decks ship a fixed 16:9 stage that scales to its
   viewport, so the tall scroll-preview frame letterboxed it and showed a dark
   --stage-bg band above/below the slide. Tag deck cards with data-od-mode and
   give them a 16:9 frame the iframe fills natively (no transform, no pan), so
   the stage sits edge-to-edge with no band.

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

* fix(home): deck preview frame to 16:9 so no white strip below the slide

The deck preview override targeted the inner html-frame while the gallery frame
kept its fixed 300px height, leaving a white strip under the 16:9 slide. Make
the gallery frame itself 16:9 for deck cards so the stage fills it exactly —
title bar + slide, nothing left over.

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

* fix(decks): hide the page-counter chrome on all 23 community slide decks

The five upstream deck families each render a different page-counter ("1 / 10")
— a center pill (.deck-controls), an id="counter" badge, a fixed .deck-counter,
or a .count span inside a Shadow-DOM rail — so the home gallery thumbnails
showed it inconsistently. Per maintainer request, drop it everywhere rather
than unify: inject a display:none rule for the light-DOM counters and inline-
hide the Shadow-DOM .count span. Keyboard/touch navigation is untouched; only
the visual counter is removed.

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

* fix(home): fix deck letterbox band in the example-prompt rail tiles too

The earlier 16:9 deck-preview fix only covered the Community gallery cards; the
hero example-prompt rail tiles render the deck in a 4:3 (1440×1080) preview and
still showed a dark --stage-bg band on top. Tag deck preset tiles with
data-od-mode and give them a 16:9 preview the iframe fills natively, matching
the gallery treatment.

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

* fix(home): remove the ↗ open-external button from community gallery cards

The ↗ only rendered when a tile's preview was an HTML source, so it
appeared on some cards (most decks, a few prototypes) and not others
(images, srcless tiles) — inconsistent chrome across the shelf. Drop it
and the whole onOpenExternal pass-through (PluginCard → PluginsHomeSection
→ HomeView) plus the now-dead .plugins-home__gallery-open styles.

* fix(home): make the community search clear (×) button visible

The clear button uses the shared <Button> component, whose CSS-module
class set padding 6/12 and crushed the 12px × glyph to zero width inside
the 26px button (a blank grey square). The .plugins-home__search-clear
reset only out-specified the global `button {}` element rule, not the
component class — scope it under .plugins-home__search so the padding:0 /
20x20 sizing actually wins.

* fix(home): align deck example-prompt tile title row with other tiles

Deck preset tiles used a 16:9 `auto` preview row, so their card was
shorter than the fixed-150px sibling tiles; the flex rail stretched them
back up and the slack leaked into the title row, making deck titles ~6px
taller. Keep deck tiles on the standard 150px preview cell and center the
native 16:9 stage inside it (neutral margin, no dark band), so card and
title-row heights match prototype/HyperFrames/image tiles.

* fix(deck-bridge): mirror the visible class so reveal animations fire on navigation

Reveal-animation decks (the frontend-slides family) gate staggered
entrances on a separate `.slide.visible` class that the deck's own show()
adds alongside `.active`. The host bridge's setActive() flipped only the
active class, so bridge-driven page turns showed slide chrome but left
every .reveal child at opacity:0 — the body rendered blank. Mirror
`.visible` in lock-step with the active slide (only for decks that use it,
a no-op elsewhere). Adds a red→green regression test.

* fix(home): unpin the bare Frontend Slides template from the slides shelf

example-frontend-slides is the family-root template with a generic cover;
pinned first it read as filler leading the Slides shelf and deck chip.
Drop it from PINNED_SLIDE_PLUGIN_IDS so it falls to the uncurated tail
while the styled variants (Creative Voltage, Electric Studio, …) keep the
lead.

---------

Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 09:32:15 +00:00