Commit Graph
1615 Commits
Author SHA1 Message Date
CaprikaandGitHub ad130f37a1 [OPEND-4257] fix(web): restore pre-8408 chat artifact action styles (#8978)
* fix(web): restore chat artifact actions and align development Vela CLI

* docs: attach desktop evidence for restored chat artifact actions

* fix(web): keep localized artifact actions within narrow cards
2026-10-22 06:27:16 +00:00
c575e6e464 [OPEND-3919] fix(web): remove the home @ picker plugin hover card (#8911)
* [OPEND-3919] fix(web): remove the home @ picker plugin hover card

Hovering a plugin row in the home composer's @ picker appended a detail
card inside the scrollable results list; hovering a skill row removed it.
Near the bottom of an overflowing list that height change re-clamped
scrollTop, slid a different row under a still pointer, and the list
flickered between two scroll positions.

Product decision is to drop the card entirely. Row hover now only moves
the active highlight. Removes the card markup, its hoveredPlugin state,
the now-unused getPluginQueryPreview helper, its CSS, and the
homeHero.parameters i18n key that only the card used.

The old "Details button closes the picker" test is replaced by a
regression test asserting the results list keeps the same shape while
hovering between plugin and skill rows (red on main, green here).

* ci: retrigger CI

---------

Co-authored-by: 罗高 <luogao@gaogao.local>
Co-authored-by: 罗高 <luogao@gaogao.lan>
2026-10-22 06:16:04 +00:00
5b6fbb55ea [OPEND-3712] feat(telemetry): stability metrics for launch, project entry, and memory (#8805)
* feat(analytics): define client metrics schema and unify telemetry metadata

* feat(desktop): measure process startup phases through business readiness

Add launch_attempt, business_ready, and launch_result timing with monotonic
phase offsets, per-attempt correlation, shared app session identity, explicit
process/activation measurement boundaries, and idempotent terminal results.
Preserve packaged sidecar phases across the desktop handoff and report bounded
startup failures without blocking successful startup on telemetry.

Window mount/reveal alone does not declare business Ready. Preload/web owners
must wire od:startup:context and od:startup:business-ready with matching
launch_attempt_id/app_session_id plus required_data_ready/actions_ready=true.

Align with the shared event registry/schema and envelope fields (device_id,
event_id, app_session_id, client_type, capture_source, env, channel,
app_version, os_version, arch, occurred_at, reported_at, capability), plus
startup_type, launch_attempt_id, started_at, duration_ms, outcome, reason,
failure_stage, stage_offsets_ms, stage_durations_ms, measurement_start,
start_time_source, and os_launch_time_available. The existing daemon safety
bridge overwrites some envelope fields; shared-envelope alignment stays with
that owner.

Coverage limits: no OS icon-click timestamp or separate launcher-process span;
no new packaged launch report without real installation identity; failures
before packaged runtime context is armed and forced process exits can be
unobserved. Cold means a fresh desktop process; warm means hidden-window
restore; reopen means desktop restored from a headless owner. Measurement
acceptance and preload ownership need maintainer confirmation; native
QA/publication remain.

Validation (local Node 24 / pnpm 10.33.2):
- Red specs captured before implementation.
- desktop focused tests: 7 files, 47 passed.
- packaged focused tests: 6 files, 87 passed.
- pnpm --filter @open-design/desktop typecheck: passed.
- pnpm --filter @open-design/packaged typecheck: passed.
- pnpm guard: passed.
- pnpm typecheck: passed after building missing local workspace artifacts.
- git diff --cached --check: passed.

Local commit only. No web/memory-sampling changes or external operations.

* feat(web): measure project navigation intent to actionable ready

* feat(observability): add bounded periodic process memory sampling

* feat(telemetry): integrate launch readiness and bounded process memory metrics

Unify metric envelopes, classify process launches with a persisted heuristic, wire preload and renderer readiness, cancel abandoned navigation, and bind consent-aware memory sampling to the Electron lifecycle. Document the metric definitions and stabilize validation fixtures that inherited host configuration or raced signal-handler readiness.

* docs(telemetry): translate stability drafts to English

Rewrite the historical project-entry draft in English and neutralize the
stability-metrics intro so odcrew-facing docs stay English-only.

* fix(telemetry): distinguish Linux Node installs from packaged runtimes

* fix(telemetry): wait for committed conversation navigation before readiness

* fix(telemetry): exclude deferred headless restores from launch timing

* fix(telemetry): require current Home project and agent reads to succeed

* fix(telemetry): fail closed on damaged memory sampling consent

* docs(telemetry): note catalog read failures cannot block Home readiness

* feat(telemetry): prepare update stage and desktop restart observations

* feat(telemetry): prepare responsiveness collectors pending transport integration

* fix(desktop): cancel consent lookup and anchor memory sampling to ready

* fix(telemetry): classify and correlate crash and exit reports

* feat(telemetry): measure consented visible preview attempts

* feat(telemetry): wire page switch intent and readiness timing

* fix(telemetry): validate responsiveness metrics and fence revoked buffers

* fix(telemetry): require successful scoped Home resources before readiness

* fix(telemetry): clear chat health collection on consent withdrawal

* docs(telemetry): record integrated responsiveness validation and budgets

* fix(telemetry): admit update lifecycle stage metrics

* test(contracts): restore merged client metric schema cases

* fix(telemetry): route native stability metrics through safety capture

Remove metrics-consent checks from startup, memory, exit and update-stage
reporting while preserving product analytics consent and privacy projection.

* docs(telemetry): align stability metrics with safety-channel policy

Document consent-independent stability reporting and unchanged product/content
consent, privacy projection, budgets and remaining native QA.

* fix(telemetry): observe runtime daemon exits and correlate memory samples

Capture parent-observed daemon death through the safety channel, preserve stronger evidence across restart, and join exits to real packaged memory sampling with opaque process IDs and nullable V8 heap limits.

* fix(telemetry): send web stability metrics on the safety channel

Drop the metrics-consent gate from web responsiveness, preview and chat
health collectors to match the native safety-channel routing, and remove
test hooks for the deleted consent API.

* test(web): record an open intent before asserting a trusted first paint

* test(web): align component tests with commit-driven navigation and visible-preview gating

ProjectView switches conversations in the router onCommit callback, preview
diagnostics require a visible frame, and catalog/page-switch reads changed
their call shape; update mocks and expectations accordingly.

* fix(telemetry): require successful catalogs before reporting page navigation ready

A failed design-system/skill/template read cleared loading without marking the
catalog ready, so page switches still reported ready. Mirror the startup gate.

* fix(desktop): prune expired stage-only update check flows

Every scheduled check opens a metric flow directory. Remove stage-only flows
past the replay TTL during the existing startup scan; installer flows stay.

* fix(telemetry): isolate reporting from navigation and catalog fallback

* fix(telemetry): preserve business behavior with passive observation

* fix(telemetry): classify daemon channel by service packaged flag

On Linux CI the Node binary lives under /opt/, so execPath heuristics
marked unpackaged runs as packaged and reported channel=stable. Without
an explicit or version-inferred channel, derive it from the service's
packagedRuntime flag instead. Also restore the desktop SHOW-hook boundary
assertion to match activeDesktop.show().

* fix(telemetry): keep killed daemons and fresh-install launch attempts observable

OPEND-4221: the sidecar supervisor exited 0 whenever its target died from a
signal, so an external SIGKILL of the daemon reached packaged as
code=0/signal=null and was reported as clean-exit. The supervisor now exits 0
only for stops it forwarded itself and encodes any other terminating signal as
128 + N; observeDaemonExit decodes that back to reason=killed with the signal.

OPEND-4220: packaged flushed client_launch_attempt directly at daemon-ready and
dropped it when installation.json did not exist yet, which is always the case
on a fresh install. Launch events now stay buffered until desktop main arms the
daemon-backed reporter, and the daemon retains launch metrics (bounded, in
order) until the installation identity exists, replaying them before later
events. Explicit opt-out still delivers immediately on the safety channel.

* fix(telemetry): exclude url_load previews from the preview denominator

trackVisiblePreview probes the frame with od:preview-metric-probe and, after
15s without od:preview-metric-result, finishes the attempt as error /
probe_unverified. URL-loaded artifacts never receive the srcdoc observability
bridge, so they cannot answer; every visible, successful URL preview was
therefore counted as an error. Frames that cannot answer the probe are now
excluded from the denominator, matching how the PR scopes Live Artifact HTML.

* fix(telemetry): move renderer safety identity to the installation id on fresh install

OPEND-4220 retest: on a fresh install the renderer safety channel bootstraps
before the installation id is minted, falls back to the anonymous id, and kept
it for the whole session, so renderer stability events carried a different
device_id than native launch/memory events. setIdentity now also patches the
safety context, and a late bootstrap keeps the installation id.

---------

Co-authored-by: heggria <bshengtao@gmail.com>
2026-10-22 05:55:52 +00:00
75693e95f4 [OPEND-3114] feat(od-next): open registered agents and CLI BYOK (#8702)
* feat(od-next): open registered runtimes and CLI BYOK

Use the effective runtime registry for OD Next admission and remove retired
capability qualification. Keep native observation and existing resume/cold
recovery, with the evidence graph owned by observability contracts.

Add run-scoped BYOK connection flags with caller-selected environment key input
and reuse the existing provider bridge. Accept equivalent frozen Skill JSON
and Base64 representations while preserving content and ownership boundaries.

Validated with focused contracts, runtime, public Run and CLI regressions;
eight real native agents and a real BYOK request/production pair; guard and
workspace typecheck. Refs #8701.

* fix(web): describe BYOK project file tools

Explain that API-provider project runs use OpenCode file tools and require
OpenCode to be available. Update the existing notice in all 19 locales and
its component/browser regressions.

Validated with red/green notice checks, real settings and preview follow-ups,
guard and workspace typecheck. Refs #8701.

* fix(od-next): preserve hidden prototype form feedback

Keep HTML hidden semantics within the shared layout layer without overriding
authored product CSS or hidden-until-found behavior. This fixes name errors
remaining visible after a successful registration.

Validated with a red/green Chromium regression, three resource-boundary
checks, a real Claude follow-up with desktop/mobile form interaction, guard
and workspace typecheck. Refs #8701.

* test(od-next): make Run fixtures select their execution path

Explicitly select ordinary Runs for retry persistence, plain stdout replay
and the compatibility single-run telemetry case. Keep task hierarchy and
terminal fallback telemetry cases active, and assert the selected path.

Restore owner write permission to the leftover-agent fixture's frozen
directories only during teardown so recursive deletion works on POSIX.
Skip symlinks and preserve cleanup errors and every original behavior check.

Reproduced all four CI failures before the fix. Focused daemon and E2E
checks passed (12 tests), along with guard, workspace typecheck and focused
LSP diagnostics. Refs #8701.

* fix(daemon): avoid passing Pi resource directories as prompt files

Pi reads existing --append-system-prompt paths as files. Stop passing skills, design systems and task input directories there; their resource paths already travel in the composed RPC prompt.

Validated with 104 focused runtime/RPC tests, a real Pi 1.1.0 offline startup red/green probe, workspace typecheck and guard.

* fix(web): coalesce streamed calls in generation progress

Fold in-flight snapshots and terminal inputs through the existing tool-event reducer before rendering the Design Files progress feed. Keep the latest input, stable call identity and the limit of twelve distinct calls.

Reproduced the duplicate React key warning before the fix. Runtime and progress-feed regressions now pass alongside existing in-flight tool-row tests.

* fix(web): preserve forms and browser storage in prototype previews

Permit client-side form submission in opaque interactive previews. Route Web Storage artifacts through the existing isolated powered preview, including daemon hints for large files, and retain guards when isolation is unavailable.

Verified URL and srcDoc form interaction, persisted values after reload, and host isolation with focused Chromium regressions. Focused viewer/runtime and daemon tests, workspace typecheck and guard passed.

* test(web): keep srcDoc fixtures on the opaque preview path

Use an existing non-storage sandbox-shim signal in transport lifecycle fixtures now that Web Storage artifacts select the isolated preview origin. Preserve every reload, activation, recovery and manual-save assertion.

Validated all 338 cases in the three affected FileViewer test files, workspace guard and typecheck, and focused diagnostics.

* fix(byok): accept keyless IPv6 loopback providers

Recognize the bracketed IPv6 hostname returned by WHATWG URL parsing in both settings validation and the OpenCode BYOK runtime. Keep remote hosts key-required.

Validated with focused daemon and Web tests, settings browser checks, workspace typecheck and guard.

* fix(daemon): restore Pi sessions and preserve completed RPC turns

Load prior Pi history with switch_session and capture the authoritative sessionFile through nonblocking get_state. Treat a host-requested SIGTERM after a clean completed turn as shutdown, including Pi builds that report exit 143, while preserving real errors and cancellation.

Validated with focused protocol and public Run lifecycle tests, a real Pi generation and native resumed edit, workspace typecheck and guard.

* fix(preview): isolate project storage and preserve hidden controls

Give powered previews stable per-project loopback origins and reserve those hosts for matching powered GETs before API or static exceptions. Advertise the capability so newer Web clients retain the opaque fallback with older daemons.

Reuse the active powered transport in Present. Share a low-specificity hidden-control guard across URL and srcDoc previews without overriding explicit author rules or until-found; preserve doctype mode when synthesizing a missing head.

Validated with actual two-project storage, Workers, reload, Present and API isolation browser witnesses, form/hidden regressions, focused Web and daemon checks, workspace typecheck and guard.

* test(e2e): align public canaries with best-effort OD Next

Read the remaining recipe identity marker without the retired serialized package hash. Make the fake OpenCode help probe finite. Exercise the current public question-form action, completed question turns/new answer tasks, and no-artifact normal endings while retaining cancellation, artifact and deck assertions.

Validated with six fake-runtime cases and focused active/off public browser canaries with explicit rollout flags; no production completion policy or timeout was relaxed.

* fix(od-next): guide focus restoration after list updates

Prefer native modal lifecycle and restore focus to a current control after list rerenders, falling back to the primary action when a filtered item disappears. Real OpenCode and Pi prototypes exposed detached-trigger focus loss.

Keep this as best-effort build guidance, without output contracts or automatic repair rounds. Validated the actual artifacts through normal public edit tasks and browser interaction checks; profile resource tests passed.

* fix(od-next): retain focus through inline task updates

Extend the existing live-control guidance to inline list actions. Real Copilot and Qoder boards also lost keyboard focus when status updates rebuilt their cards; hidden filtered items need a visible fallback.

* fix: align Discovery integration tests and presentation storage

* test(daemon): tolerate transient memory origin cleanup races

---------

Co-authored-by: zhouzheng <zhouzheng@refly.ai>
2026-10-22 05:14:59 +00:00
CaprikaandGitHub 4d4f53fdf4 [OPEND-4150] fix(share): refresh previews during comments and unblock publication (#8904)
* [OPEND-4150] fix(share): unblock updates and resolve native HTML anchors

* [OPEND-4150] fix(share): refresh comments live and decouple publication

* fix(comments): preserve save identity and HTTP UUID compatibility

* fix(comments): save current replacement styles

* fix(comments): clear unfinished strokes on URL preview load

* fix(comments): validate chat targets and refresh slide indices

* test(comments): cover send validation after main integration

* fix(comments): bind target validation to document generation

* fix: preserve normal comment saves during preview refresh (OPEND-4150)
2026-10-22 05:12:50 +00:00
341641d71a [OPEND-3962] fix(motion): 修复预览声音与模板配乐约束 (#8777)
* fix(motion): avoid audio seek thrashing during preview stalls

* fix(motion): keep in-progress video previews silent

* fix(motion): let user briefs override template demo soundtracks

* fix(motion): keep project grid covers muted

All Projects grid and recent-project hover covers loaded motion sources
without odMotionThumbnail, so the embedded player autoplayed the
soundtrack. Covers now always request thumbnail mode: animation keeps
playing, audio stays muted until the project preview is opened.

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
2026-10-22 04:53:43 +00:00
Ray XiandGitHub 938a09b12c [OPEND-3334] feat(web): one project view from the Home send — no hand-off, one Working state (#8267)
* fix(web): keep the project split still across the Home hand-off

The optimistic creation frame left `.split` on its stylesheet default
(460px) while ProjectView resolved the saved width, or the equal split of
the container, only after it mounted — and that first write ran through
the 200ms width transition, so the chat column moved once the real view
arrived (OPEND-3207).

Move the split geometry into `project-split-layout.ts`, have both surfaces
resolve their first-paint width through `resolveProjectSplitLayout`, and
settle a surface's first measured write without the transition.

Red spec: e2e/ui/home-send-split-width.test.ts (red on main, green here).

* test(web): read the 4px handle constant from project-split-layout

The material spec pinned `SPLIT_RESIZE_HANDLE_WIDTH = 4` in ProjectView's
source; the constant now lives in project-split-layout.ts and ProjectView
imports it, so assert on the module and on the import.

* test(cms): wait for the Test modal impression before switching language

The locale case only passed while the impression frame never fired: on a
fast machine the next language switch unmounted the modal before its
requestAnimationFrame recorded the impression. CI is slow enough for the
frame to land, and the switch that follows then closes the modal for good.
Waiting for the recorded impression makes that order deterministic.

* fix(cms): keep the open Test modal across a same-deployment reload

A language switch (or lease renewal) reloads the selected Test deployment.
While the new decisions are in flight the runtime publishes a session
without decisions, and the modal treated that gap as the presentation
ending: it dropped its "open" marker, then the returning decision looked
like a new offer of an activity this account had already seen and stayed
closed.

Key the open presentation by the selected deployment and snapshot. A
reload of the same selection continues it; a redeploy, a different
account, a dismissal, or the session ending still releases it.

* test(e2e): red spec for the Home → project hand-off parity (OPEND-3334)

Two pins for the hand-off between the optimistic project frame and
ProjectView: pixel parity of the chat and workspace columns across the
hand-off (animations frozen), and a single assistant status label + class
set from the click to the first content. Expected red on main: the frame is
a hand-drawn copy of ProjectView and drifts from it (old "Preparing…" footer
vs the execution record's "Working" head, simplified composer, disabled
empty-state pills, tabs dock, switcher name).

* test(e2e): enroll the Home hand-off specs in the ui_p0 entry-settings group

home-amr-pending, home-send-single-loading and home-send-split-width were
merged (or opened) without being enrolled in uiP0Groups, so no merge lane
had ever executed them; only the prerelease full pool would have. Enroll
them with the other Home-entry files.

* test(e2e): enroll the hand-off parity spec in ui_p0 (OPEND-3334)

* feat(web): draw the Home → project hand-off frame with the real view's parts (OPEND-3334)

The optimistic creation frame was a hand-drawn copy of ProjectView and had
drifted from it, so the hand-off moved: the old assistant footer's
"Preparing…" flipped to the execution record's "Working" head, the composer
gained its accessories, the empty-state pills lit up, the tabs dock gained
the history control.

The frame now draws the same parts: the role row named through the shared
`assistantRoleNameForAgent`, the real `ExecutionShell` on an empty running
shell ("Working", its orb, its geometry), the real composer accessories
(design-system picker + agent/model menu) inert, the history control's
footprint in the dock, and live-looking, inert empty-state pills. Behind an
execution shell the old footer no longer paints its running label, so a
running turn states its status once.

App and ProjectView pass the agent name, config, agents, daemon liveness and
design systems the frame needs; the pending record carries the send's
design-system id.

* test(e2e): compare the status row's element set per hand-off phase

The record head is more than one element (its shimmer span and the summary
wrapper both carry the text), so counting distinct class sets across the
whole log flagged an identical head as a change. Compare the set the frame
drew with the set the view drew instead.

* feat(web): let the real ChatPane draw the Home hand-off's first turn (OPEND-3334)

The pending frame and ProjectView's hand-off card were hand-drawn copies of
the chat column, so every ChatPane internal they missed (the fixed composer
layer, the chat seam, the log's top inset) was a jump at the hand-off. Both
now render the real ChatPane on an optimistic first turn built from the
creation record: a user message and a running, empty assistant placeholder.

- runtime/chat/creation-handoff: pure builder for that turn; staged images
  preview through object URLs until the files are in the project.
- ChatPane: `detached` mode (inert pane + composer layer) for the frame;
  the composer portal target resolves at mount so the composer is portaled
  before the first paint instead of one frame after it.
- ProjectView: mounts ChatPane during the hand-off and feeds it the
  optimistic turn until a real message exists; the overlay card, the hidden
  pane CSS and `composerLayerHidden` are gone.
- agentModelSelection: one `selectedAssistantIdentity` for the real and the
  optimistic turn.

* fix(web): give the pending frame's workspace column the add-tab and project-menu controls (OPEND-3334)

The parity pin now passes on the chat column and fails on the workspace
column: the frame lacked FileWorkspace's add-tab button and the project
menu trigger, so the top bar was 4px shorter and the empty state below it
moved at the hand-off.

* test(web): decouple pending handoff assertions from CSS classes

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

* fix(web): the Home hand-off's first turn fades in once, no remount, no workspace reload (OPEND-3334)

A rAF probe of the Home send found the first turn mounted four times, each
playing the 200ms `msg-enter` fade (QA's recording shows the flashes):
the pending frame (the one intended entrance); ProjectView taking over;
the ChatPane key moving from `conversation-unavailable` to the resolved
conversation id; and the real rows replacing the optimistic ones under new
ids. The workspace went pending-empty -> "Loading..." -> empty.

- ChatPane `quietEntrance`: rows first rendered while it is on get
  `msg--quiet-enter` (no fade) for their lifetime; dropping the flag never
  restarts an animation. ProjectView sets it while the hand-off runs.
- ProjectView keeps the pane's key when the hand-off pane's first
  conversation id resolves; a later switch still re-keys.
- A project born from the Home send (not an imported folder) shows the
  empty workspace until the first listing, as the pending frame did.

* test(e2e): pin one entrance, one view pane and no workspace reload across the Home hand-off (OPEND-3334)

* fixer: address PR #8267 follow-up items

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

* fix(web): keep the file-list authority gate across the Home hand-off (OPEND-3334)

e3596b032c made a Home-created project's workspace authoritative before
its first listing was accepted, so the empty-state CTAs were live while
Home attachments were still uploading, and a failed first read looked
like a genuinely empty project (review by nettee).

Restore `filesAuthoritative={committedFilesGeneration > 0}`. Until that
first listing, `emptyLookBeforeFirstListing` draws the same empty state
the hand-off frame showed, from DesignFilesPanel's one empty-state
markup, but inert and aria-busy and without CTA test ids. The live empty
state (or the files) replaces it once a listing is accepted, so the
workspace still never detours through "Loading…".

* feat(daemon): accept a caller-minted seed conversation id on project create (OPEND-3334)

* refactor(web): mount ProjectView from the Home send and hold project reads until the create answers (OPEND-3334)

* test(web): pin the held reads and the single pane across the Home send (OPEND-3334)

* test(e2e): key the Home hand-off specs on the creation phase, pin one pane and one user row (OPEND-3334)

* test(e2e): no project-scoped request leaves before the create answers (OPEND-3334)

* test(web): type the fetch spy in the held-reads case

* fix(web): hold the pending-prompt clear, tab hydrate, folders and share-status reads until the create answers (OPEND-3334)

The e2e network assertion caught five project-scoped requests leaving
before POST /api/projects answered. A Home send's view is also an
auto-send from mount now: App writes the auto-send keys after the
create, so the prompt must not fall back to a composer draft.

* fix(web): keep the run-status feed off a project until its create answers (OPEND-3334)

The rail and the tab switcher list the optimistic row from the click, and
their shared feed polled /api/runs?projectId= for it every 4s. The read
predates this branch; the e2e network assertion caught it intermittently.

* fix(web): the optimistic first turn belongs to ProjectView, not App's creation record (OPEND-3334)

QA on bd5fe20579: App releases the creation record 8s after the create;
Home uploads outliving that made the first turn and its Working row
vanish until they finished. The view now captures the turn at mount and
retires it when the real turn owns the rows, when the auto-send is turned
down, or when the record is gone with nothing left to send. The seed
conversation id is kept for the view's life so the pane never re-keys.
2026-10-22 03:52:55 +00:00
b354e88704 fix(web): restore task type pills and downward menu (OPEND-4215) (#8947)
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
2026-10-22 01:48:32 +00:00
open-design-crew[bot]GitHubbone3deep1962-collabCheemsCheemszhouzhengWilliamopen-design-crew[bot] <4197780+open-design-crew[bot]@users.noreply.github.com>NJUHua
3dfd41c6b3 [OPEND-2800][OPEND-4117] feat(od-next): integrate Discovery delivery and validated prototype fixes (#8804)
* fix(od-next): continue plans with keyed markers and honor explicit deliverables

* fix(od-next): retire output contracts and hand off continued tasks

* fix(od-next): record round settlement and recover production cold

* fix(od-next): resume marker sessions with context deltas

* fix(od-next): separate completion facts and honor plan-only intent

* fix(od-next): preserve physical failures and legacy chat compatibility

* test(web): align run recovery fixtures and await campaign mount

* fix(od-next): tolerate terminal production marker formatting

* fix(od-next): unblock follow-ups after a stop or unreadable history (#8423)

Treat a previous task this build cannot read as absent so the follow-up starts a new task, and stop counting a Run with a pending cancel as holding its conversation. Add end-to-end coverage for the marker continuation flow and restore the remote Workspace directory gate cases.

* feat(od-next): add deliverable discovery and ten-type routing

* fix(od-next): refine discovery and live delivery evidence

* feat(observability): expose actual task delivery receipts for evaluation

* fix(observability): settle successful headless task telemetry

* docs: clarify focused local validation

Keep local daemon and web validation scoped to affected files and integration boundaries. Reserve full suites for PR CI unless the user explicitly requests them.

* feat: add Motion Design skill and OD Next task route

* fix(web): rename motion design entry and place it second

* fix(web): reconcile motion design catalog consumers

* feat(web): add motion design templates to Home and Community

Five motion design example templates (3D procedural island, explainer,
product launch, showreel, data story), chosen by community reach, under the
Home motion-design chip and a new Community "Motion design" type.

Each template follows the bundled example layout: an OD-generated
example.html with its runtime libraries under assets/, an HTML preview baked
by the existing preview pipeline, and a SKILL.md that carries example.html
as the reference when the card is picked on the OD Next motion-design route.
Home cards ship curated copy for every UI locale. Motion templates stay off
the Video and HyperFrames chips.

* fix(skills): keep motion-design procedural layers visible in video export

The MP4 exporter seeks timelines with callbacks suppressed
(renderSeek(t, { suppressEvents: true })), so content drawn only from a GSAP
onUpdate callback is missing from exported video. Recommend a per-frame draw
loop that reads the timeline clock, and state that visible content must not
depend on timeline callbacks. The OD Next task asset is re-synced with
scripts/sync-motion-design-skill.ts.

* fix(motion): preserve reference assets and exported animation frames

* feat(skills): integrate curated motion helpers with complete runtime files

* feat(skills): add Huashu, Oil Motion and Motion.Lab adapters

* test: classify animate design guidance in narration guard

* feat(motion): refine five community templates and portable previews

* fix(motion): refine launch transitions and center gallery previews

* fix: allow free-form briefs on automatic motion task route

* feat(motion): add template soundtracks and audio export

* fix(motion): require rendered MP4 alongside editable video source

* test(motion): align strategy activation expectation with package version

* fix: scope motion delivery and make editable sources playable

* fix(motion): bundle source dependencies and valid closing protocol

* feat(motion): rebuild the five templates on the most-viewed community posts

Re-select topics by single-post views in the latest awesome-opus-5.5-video
snapshot and anchor each template to the top eligible post of its genre:
camera-focus explainer, showreel, product launch, procedural island and a
new wordless animated short that replaces the data story.

All five example.html files are hand-authored canvas/Three.js films drawn by
one render(t) from the GSAP master timeline, so preview, scrubbing and MP4
export produce identical frames. Each ships an original procedural
soundtrack, updated SKILL.md and 19-locale copy; Home order follows reach.

* fix(plugins): load bundled fonts in plugin previews

Plugin previews blocked every bundled font: the preview CSP had no font-src,
and relative url() references inside inline <style> blocks kept pointing at
the preview URL instead of the plugin folder. Allow same-origin fonts, map
inline-style url() references through the existing asset route (escapes,
absolute and data: URLs stay untouched), and serve font files with font
MIME types.

* feat(community): make motion template Use match the Home example pick

Community 「做同款」 on a motion design template now binds it as that type's
own example card, carrying the type's project metadata, so the run takes the
same OD Next motion-design route as a pick from Home's motion design list and
delivers the MP4 together with the editable source. Other task types keep
the existing plugin-pinning hand-off.

Community use clicks now record use_source (card | detail_modal) and
use_action (use | use_with_query); the details modal Use is tracked for the
first time. The Home hand-off no longer records a second community_gallery
click for uses Community already tracked.

* fix(plugins): count the five motion templates in the official registry metadata

* fix(media): keep HyperFrames audio filters compatible with FFmpeg 4.1

CI's bundled Linux FFmpeg 4.1 rejects adelay all=1 and amix normalize=0.
Make each track stereo and delay both channels explicitly, skip amix for a
single track, and undo amix averaging with a gain for several tracks.

* fix(motion): keep template previews bakeable on slow CI renderers

CI's software-GL baker skipped the 3D island (preview readiness timeout):
under slow frames GSAP lag smoothing slowed the timeline, so the 8 s
poster frame never arrived within the baker's 15 s wait. Play every
template on the wall clock and move the island poster to 6 s. With
simulated 300 ms frames all five now report ready within 10 s.

* feat(motion): add the kinetic typography template

Template selection now follows the motion design spec's four content types
(brand and product films, kinetic typography, explainer animation, procedural
art). Kinetic typography takes the fifth slot, replacing the animated short.
The new template is a 15-second, 120 BPM type film with an original
soundtrack and 19-locale copy.

* fix(motion): let the project motion player fit template stages

Inside a project the motion source player wraps the composition, fits it and
adds its own controls. The templates also scaled their stage, so editable
sources from Remix and Community use rendered at a fraction of the preview.
Templates now keep their own fit and controls only outside that player.

* feat(motion): restage the 3D island as a midday lagoon

The island moves from golden hour to a bright tropical midday closer to its
anchor post: a shader lagoon coloured by a baked depth map with ripples,
shallow-water shimmer and shore foam, a wide white-sand beach, palms and
instanced bushes, a village with a pier, soft sun shadows and a lower
camera path. The CPU wave mesh is gone, so frames cost no more than before.
Copy is updated in all 19 locales.

* fix(motion): keep each soundtrack inside the composition root

A Community use regenerated the island source tail and dropped the root
<audio> element, so its MP4 had no sound. Templates now place the element
inside the composition root next to the stage.

* docs(motion): state the template selection rule as complete works

* fix(motion): enforce video clocks and bound content verification

* feat(motion): replace the motion templates with remakes of viral posts

The five Motion design templates are now frame-faithful remakes of viral
motion posts, so each preview shows the look users came for: the 15-second
motion designer showreel, a high-energy motion reel (copy set in English), a
one-shape UI motion piece, a pixel-art wizard and a 20-second electric-blue
tech reel. Each ships open-licensed fonts, an original soundtrack, the
creator's published prompt where one exists, and credits its source post.

The explainer, product launch, 3D island and kinetic type templates are
removed. Tests follow the new set; a backward-seek check now covers hidden
WebGL layers.

* feat(motion): feature The Future Is Already Here first and start preview sound with the film

* chore: ensure PR CI

* fix(motion): match The Future Is Already Here controls and reorder the motion templates

* fix(motion): make the one-shape UI preview controls legible

* fix(motion): hide The Future Is Already Here controls inside the project player

* fix(od-next): maintain discovery prompt assets in English

* fix(motion): plan suitable sound and synchronize source audio playback

* feat(motion): keep remake soundtracks by default and fade exported audio where it stops

* test(motion): cover audible source controls in Chromium

* fix(test): use explicit ESM imports in audio player regression

* fix(od-next): preserve planning intent and make agent CLI probes inert

* fix(motion): align preview track discovery with video export

* feat(motion): autoplay looping HTML without a player bar

* fix(test): consume the built motion playback contract

* docs(motion): state explicit soundtrack rules for make-same and the music line in replies

* chore(motion): version the audible looping delivery profile

* docs(motion): let template sound rules lead the general policy and freeze the audio contract

* fix(motion): scope autoplay to declared video motion sources

* fix(motion): reject audible MP4s under an explicit silent contract

* docs(motion): drop the routine music note from make-same replies

* test(motion): align delivery regressions with looping source playback

* fix(od-next): route refreshable sources and preserve live templates

* fix(od-next): distinguish source refresh from preview reload

* fix(motion): isolate preview and audio export from legacy HyperFrames

* fix(od-next): retain live list containers for empty source data

* test(motion): retain the existing desktop legacy audio limitation

* fix(motion): suspend audio when previews leave the active workspace

* feat(motion): replace two motion templates with a product promo and a visual lesson; sign the showreel as Open Design

* fix(discovery): distinguish reference recreation from new prototypes

* fix(discovery): require skill reads before copying reference artifacts

* feat(motion): give the showreel the Open Design palette and a full-size stacked wordmark

* feat(motion): give the product promo a livelier mascot girl

* refactor(motion): rebuild The Future Is Already Here as readable, editable scene code

* fix(motion): pick gallery cover frames that survive the baker's capture delay

* feat(motion): sharpen the showreel opener, depth, truchet and end card

* fix(discovery): avoid implementation-only skills and false video delivery

* feat(motion): make the product promo mascot talk, add a speaking score and the flow pop

* fix(discovery): distinguish video rendering from editable animation delivery

* feat(motion): give the visual lesson the original's paper-collage texture

* fix(motion): bundle gallery covers for fresh branch checkouts

* feat(motion): feature and refine Bring Ideas to Life

Adapt the paper story around OpenDesign capabilities, with short rounded hands, clearer timing, continuous paper actions, and a larger audience invitation. Feature it first in Motion and synchronize localized names, metadata, cover, guidance, and regressions.

Keep product promo audio music-only with sparse percussion.

* fix(motion): stabilize artifact cards and capture declared covers

* fix(discovery): retain WebGL intent and isolate reference-only skills

* fix(discovery): complete authorized plans and verify audible delivery

* fix(discovery): gate skill reads on current production authorization

* fix(web): share creation type order with community

* fix(discovery): stop required-model attempts on authentication failure

* fix(discovery): retain required model provider and billing channel

* fix(discovery): stop opaque video failures without diagnostic generations

* fix(discovery): distinguish local state and tighten output adherence

* fix(preview): preserve local forms and hash routes in strict sandbox

* fix(prototype): keep validation feedback selectors consistent

* fix(prototype): preserve native hidden feedback states

* fix(discovery): align local routes and live filter states

* fix(prototype): preserve dynamic controls and dialog visibility

* fix(prototype): keep review and confirmation state coherent

* fix(web): stabilize preview activation and retain scoped local progress

* test(web): verify persistent previews through activation messages

* feat(web): refine task type selection and keyboard navigation

* fix(discovery): distinguish execution from requested delivery and expose recovery

* fix(web): use final run delivery state in folded task history

* revert(web): restore todo-based continuation controls

* revert(od-next): remove experimental continuation recovery changes

* fix(telemetry): bind Task traces to authenticated Vela identity

* test(telemetry): establish accepted Task roots in bridge fixtures

* test(discovery): align required CI fixtures with integrated behavior

* fix(web): preserve succeeded messages when run probe is unavailable

* test(discovery): verify selection clearing and production turn recovery

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: Cheems <hihicheems@gmail.com>
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: zhouzheng <zhouzheng@refly.ai>
Co-authored-by: William <zhouhaoran@refly.ai>
Co-authored-by: open-design-crew[bot] <4197780+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
2026-10-21 09:05:01 +00:00
dd43a3cef1 [OPEND-4136] feat(web): use Feishu community links for Chinese locales (#8884)
* feat(web): route Chinese community entries to Feishu

* fix(web): match Feishu shortcut icon to surrounding monochrome icons

---------

Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
2026-10-21 08:21:00 +00:00
45a6fd80b2 fix(cms): stop comparing Test tester member id with the app account id (#8892)
The Test runtime context carries the server-authenticated workspace member
id (testerMemberId), but the client compared it with the app account id
(sessionSubject). The two are different identifiers, so a valid context for
a real tester was always rejected as context_identity_invalid, and the
409 drift recovery path could never succeed for the same reason.

Pin the tester to the first acquired context within one selection instead;
the account boundary stays enforced by the selection fence (the account id
is part of the scope, so an account switch discards late responses).

OPEND-4143

Co-authored-by: heggria <bshengtao@gmail.com>
2026-10-20 10:52:52 +00:00
CaprikaGitHublefarcena1chztPerishCodeopen-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>alchemist
2b41bc5c3d feat(share): publish files as public links with stop/resume and synced comments (#8408)
* wip: wire deleted-file stop retry and cover owner-scoped HTTP behavior

* wip: persist published comment backfill state

* wip: connect backfill delivery receipts and preserve completed batches

* wip: expose exact-file backfill state and retry through HTTP and CLI

* wip: wire scoped comment sync pause into preview panel

* test: cover backfill terminal rejection and stale publication receipts

* wip: separate published content from unavailable viewer origin

* feat: distinguish publication success from unavailable viewer links

* fix(web): gate export share guide on authoritative project history

* test: restore publish backfill HTTP proof with current publisher pipeline

* fix(web): show authoritative stopped-share guidance

* fix(web): restore publish help before creating a share

* fix(web): show unread comments on collapsed rail

* fix: share annotated selectors across URL and srcdoc bridges

* test: verify saved comment selector reanchors in Chromium

* fix(web): warn about active shares before project deletion

* fix: authorize and bootstrap public share project bindings

* wip: prepare dispatched live share-chain probe (not run)

* fix(web): deliver per-file share residuals after deletion

* fix(web): prevent stopping shares in read-only viewers

* fix(e2e): retain complete share probe HTTP evidence

* fix(web): name shared projects in delete confirmation titles

* docs(e2e): clarify live share acceptance and human login boundaries

* fix(e2e): preserve share query context in diagnostic evidence

* fix(web): restore share-panel copy and comment-pin visuals per board8

chain1-publish G4: delete the stale "仅支持分享单个文件,内嵌图片等本地
资源暂不支持" sentence from fileViewer.publishSingleFileDescription (19
locales) — it contradicts the current multi-resource share and G4 required
its removal.

chain1-publish S1: add the missing "链接访问" title row + its static
description ("接收者可查看预览和已有评论,不包含对话和代码。") to
ShareTab's public-share section, as new fileViewer.linkAccessTitle /
linkAccessDescription keys (19 locales). No switch added — that control
overlaps the existing "停止分享" button and is left as a 需确认 item.

chain4-owner 4.2b: canvas comment pins go from 20x20/weight 600/2px white
ring/fixed #282828 to 28x28/weight 500/no ring, and now derive their
background/foreground from the same per-author color used by that author's
sidebar avatar (commentAuthorAvatarColor), both pure local CSS/TSX with no
packages/ or protocol change.

chain4-owner O7: formatCommentTime now buckets by calendar day (adds
"昨天", drops the "N 天前/N 周前" buckets, switches the hour bucket to
same-day instead of <24h elapsed, and formats older dates via
toLocaleDateString with year only when it differs from the current year),
and comment-side-time gets a title attribute with the full absolute
timestamp on hover.

Verification: pnpm --filter @open-design/web typecheck passes. Not run
through a real renderer — no pixel-level claim.

* fix(web): share entry icon, delete-confirm title, OP2 read-only hint

chain1-publish E0: swap the share toolbar entry's icon from RemixIcon
share-forward-line to the board's own upload-arrow glyph (13px path
"M12 4v11M7 9l5-5 5 5M5 14v5h14v-5") — same size/shape as the board's
.shareBtn icon, glyph only, no behavior change.

chain6-dialogs D-04: drop the extra "确定" from designs.deleteConfirm's
zh-CN/zh-TW title ("确定删除「{name}」?" → "删除「{name}」?") to match
the board and every other locale, which already read "Delete "{name}"?"
without an "are you sure" prefix.

chain4-owner OP2: add the missing "分享页评论不可编辑" explanation line
under a comment's textarea specifically when the viewer can't edit it
AND the comment is a share-page (external) one — team-member-reads-
external-comment (OP3) is unaffected, matching the board's OP2-only
scope. New comment.sharePageCommentReadOnly key, 19 locales.

Verification: pnpm --filter @open-design/web typecheck passes. Not run
through a real renderer — no pixel-level claim.

* test(web): update FileViewer specs for the G4/O7/4.2b restorations

Three specs encoded the exact pre-fix behavior the previous two commits
intentionally changed, so they went red:

- "reads the publish help without publishing" located the help bubble by
  the now-deleted stale sentence ("Only a single file can be shared for
  now..."); relocates it by the sentence that remains.
- "keeps relative comment-time boundaries stable across clock boundaries"
  asserted the old <24h-elapsed hour bucket; the O7 rewrite buckets by
  calendar day, so the fixture's 23h-ago comment (which crosses this
  fake clock's midnight) now correctly reads "昨天" instead of "N 小时前".
- "keeps D1-D4 marker and deck-page presentation on the rendered
  selectors" asserted the old 20px/weight 600/white-ring pin CSS; updated
  to the board's 28px/weight 500/no-ring geometry.

Verification: pnpm --filter @open-design/web typecheck passes;
tests/components/FileViewer.test.tsx: 334/334 passed (via
node_modules/vitest/vitest.mjs --pool=forks, run directly against the
node 24.18.0 binary — this shell's `npx`/`node` wrap nvm in a way that
blows bash's FUNCNEST under vitest's fork pool).

* feat(web): draw the export guide's countdown, title and description

The 10s countdown already existed as state - remainingMs, checkedAt and
paused were computed and consumed - but nothing rendered it, so the guide
showed no sign that it was about to dismiss itself.

The bar is one CSS width transition per run rather than a per-frame timer:
the remaining time is already known when the run starts, so animating to
zero over exactly that duration needs no re-render, and a pause just freezes
the transition.

Board copy added across all 19 locales.

* test(web): update AfterExportShareGuide specs for the P1 card restoration

Three assertions encoded the pre-restoration visual/DOM shape that
d2c72f1624 intentionally changed to match Board8 Chain-6-dialogs.dc.html's
P1 ("导出完成 · 分享引导条", `.s-m1eexportprompt .prompt[role=status]`)
mockup:

- role query changed region -> status: P1's `.prompt` div in the board
  markup carries `role="status"`, not the implicit `role="region"` that
  came from the old bare `<section aria-label>`.
- button count 3 -> 2, and the close-button-then-never-show sequence was
  replaced: the board's P1 card has no `.pclose` element (unlike the
  discarded EX1 alternative, which does have one) - its only controls are
  the `.pfoot` pair (`.pghost` "不再提示" / never-show, `.paction` "去分享"
  / open-share). The guide is still dismissible via never-show, open-share,
  or the unchanged 10s auto-collapse in after-export-share-guide.ts.
- dropped the dead `awaitingDesign` fixture key/assertion now that the P1
  design reference is in hand and the placeholder key has been removed.

Scoped run: 3 files / 40 tests passed via direct-node vitest (avoids the
nvm-wrapped node FUNCNEST issue under the fork pool noted in
od-side.md).

* feat(web): render the backfill, align, and shareStopped comment sync banners

CommentSyncBanner.tsx previously only rendered K8 (sessionMissing). The
backfill (K2), align, and shareStopped fields already existed on
CommentSyncState with i18n copy standing by, but no render branch consumed
them — this was flagged in .tmp/share-p0/visual-static/chain1-publish.md (K2)
and chain4-owner.md ("补传横幅/评论对齐横幅/分享已停用横幅").

Adds three branches, single-slot priority sessionMissing > shareStopped >
backfill(failed) > align(diverged), respecting the hard semantics documented
on packages/contracts/src/api/share.ts's CommentSyncState:
- backfill absent/pending/succeeded never renders a warning (absence is
  "never attempted", not success); the server already filters stale-revision
  backfill records, so any backfill this component sees is for the file's
  current publication.
- align absent or 'unknown' never renders as aligned or as a problem; only
  'diverged' is actionable.
- shareStopped: null (could not be read) never renders as stopped nor as
  healthy; only `true` renders.

Retry buttons call the existing retry endpoints (POST comment-sync-state for
backfill, POST comments/align for align) and re-fetch via a new
`refreshToken` option on useCommentSyncState. The backfill retry button is
only shown when `retryable === false`, per the field's own doc comment:
retryable:true means the server is already retrying on its own.

useCommentSyncState also gained an optional `filePath` param, threaded from
HtmlViewer's `file` prop through CommentSideDock -> CommentSidePanel, because
the backfill answer is per-file and the endpoint requires filePath to compute
it (readPublishedCommentBackfill returns undefined without one) — without
this wiring the backfill branch would be permanently unreachable regardless
of the new rendering code. This is plumbing an existing parameter to an
existing endpoint, not new triggering behavior.

Copy is the parent-approved text verbatim, added as three new i18n keys
across all 19 locales (fileViewer.commentSync.backfillFailedTitle/Body,
alignFailedBody); the retry button reuses the existing preview.retry key.

Tests: new tests/components/share/CommentSyncBanner.test.tsx (12 cases)
covering all silence/render rules above. Full apps/web typecheck, guard, and
tests/components/FileViewer.test.tsx (334/334) all pass.

Not done in this commit: the ShareTab panel's own shareStopped-driven hint
(a separate, pre-existing `publicationStatus === 'stopped'` render in
ShareTab.tsx using project-share-history data, not CommentSyncState) is
untouched — it is a different signal for a different surface.

* feat(web): add the board's "link access" switch to ShareTab

chain1-publish.md's S1/S4 rows show a `role=switch` "链接访问" toggle as
part of the panel's fixed structure that the current implementation never
had (flagged under chain1-publish.md's "需要人确认" #1 and chain6-dialogs.md
D-00). The user explicitly authorized adding it and asked me to resolve its
semantic overlap with the existing "Stop sharing" text button.

Resolution: the switch reads and drives the exact same `filePublished` state
through the exact same handlers already used by the primary button/ghost
button (publishCurrentFilePublic / unpublishCurrentFilePublic) — on -> off
calls unpublish, off -> on calls publish. Because both controls act through
the same two functions on the same state, they can never show or produce
inconsistent results; this is presentation only, not a new trigger path.

Did NOT remove the existing "Stop sharing" ghost button, even though the
board's own S4 mockup has no separate one — chain1-publish.md's own
"需要人确认" #2 explicitly warns not to delete it off this board alone,
since it may belong to a different board's scope (chain2's handoff notes
mention Chain-5). Keeping both is redundant but not contradictory, per the
task's own escape hatch ("你判断不该同时存在,那就说明理由并只做一个" —
here the judgment is the opposite: keep both, and make sure they can't
diverge).

Geometry/colors from chain6-dialogs.md D-00: 32x19 track, r999, off #D6D6D6,
on #282828, 15x15 white thumb. No new i18n keys — the switch's accessible
name reuses the existing `fileViewer.linkAccessTitle` key.

Also updates the pre-existing frozen DOM-baseline fixture
tests/fixtures/share-tab/share-panel.team.html. Confirmed via `git stash`
bisection that this fixture was ALREADY stale/failing before this commit —
the earlier "链接访问" title+description restoration (commit 721cb976cf,
prior round) changed the panel's markup without updating this baseline, and
nothing in CI caught it because this file wasn't in that round's targeted
test run. This commit's switch adds one more, unrelated divergence on top of
that pre-existing one, so the fixture is regenerated from the component's
actual current output rather than hand-edited, and captures both changes at
once. The `personal` fixture is untouched: canPublishPublic is false in that
scenario (no workspace context at all), so the entire link-access section
(switch included) never renders there, exactly as before.

Verification: pnpm --filter @open-design/web typecheck and pnpm guard both
pass. tests/components/FileViewer.share-tab-extraction.test.tsx (22/22),
ShareTab.copy-button/first-publish/publish-error/scope-keyboard.test.tsx,
tests/styles/share-scope-design.test.ts, and FileViewer.test.tsx
(394 tests total) all pass. Not rendered in a real browser — see the
evidence file for the "未经真实渲染验证" caveat.

* fix: scope deletion stops to the selected published file

* feat(contracts): define share bridge v2 geometry boundary

Define authoritative OD v2 viewport geometry, fallback pin bounds, display tokens and geometry-update gates. Add 16 passing boundary tests; root guard and typecheck passed.

Known pre-existing failure: share-contract.test.ts expects SHARE_SNAPSHOT_DISCOVERY_IN_P0=false while baseline bd39802907 exports true. The identical assertion was reproduced against unchanged HEAD source; this unrelated assertion is not modified. Vela runtime and browser acceptance remain separate.

* fix(comments): create anchors for personal public shares

* fix(comments): create anchors for personal public shares

* fix(web): prefer readable element names in comment titles

Use screen labels then element-text snapshots before localized type fallbacks. Keep synthetic pin/page titles and hide internal IDs. Export a pure resolver for share-page parity without new locale strings.

Verified 10 contract tests, all 334 FileViewer tests, all 19 existing locale key sets, root guard and root typecheck. Vela adoption and browser visual acceptance remain separate.

* fix: stop published files before file deletion

* fix: explicitly resume stopped publication generations

* Restore main comment UI styling while retaining share flows

* test: verify guarded resume through real Vela CLI

* fix(web): match main comment composer surface

* fix: register personal project owner before share history lookup

* fix(web): align comment avatar colors with shared page

* fix(share): revoke deleted sources through atomic resource removal

* fix(comments): seed client avatar colors only from authoritative author key

* test(share): expect the additive publish failure detail after merging main

#8402 adds an optional closed-token `failure` to failed publish responses.
The share-origin HTTP test compared the 502 body exactly, so it now also
expects { stage: 'push', reason: 'unknown' } for a synthetic upload error.

* test(contracts): assert deployed snapshot discovery in P0

* fix(share): revoke folder publications before deleting descendants

* fix(web): normalize share controls to supported font weights

* test(web): assert manual copy fallback after clipboard rejection

* test(share): align route fixtures with original owner authority

* feat(comments): expose explicit visitor comment pull to UI and CLI

* feat(cli): expose explicit project share resume command

* fix(cli): emit JSON for files delete

* refactor(share): name the unpublish response contract

* fix(comments): preserve Vela author key in inbound avatar identity

* test(share): use valid sharedAt timestamps in owner fixtures

* fix(share): use promise stat with production folder route fs

* fix(comments): use defined theme red for unread indicators

* fix(cli): preserve workspace context error codes

* fix(files): hide internal paths in deletion failures

* fix(comments): persist local author name and reconcile cloud author key

* fix(share): align owner sharing flows with final UI audit

* test(share): verify unboxed comment sync across owner states

* fix(share): apply second-round final design review to owner share UI

- Share panel: remove every yellow warning; show the public link row above
  the copy button in published states; oversized shares stay blocked and
  explain why in the red error row.
- Global toast for update success/failure and residual stop failure, with
  final design icons and copy; share panel buttons match the design.
- Comment sync: K1 turns the primary action into the sync progress bar, K4
  shows a busy "正在同步评论…" button, K5 retry is red.
- Deployment menu layers above the share popover; export-complete guide
  rebuilt to the final design.
- Share bridge pin label accepts exactly one letter so a CJK author's
  initial can label their pin.
- Update ui-audit capture specs for the intended copy/structure changes.

* fix(share): apply third-round design review to owner share UI

- Share toast rebuilt as ShareFeedbackToast to the design: solid red/green
  badge, plain underlined 重试, close; used for update success/failure and
  the deleted-project stop failure.
- The ··· deploy menu drops down under the trigger, right-aligned to the
  header tools, over the share panel.
- Signed-out share button reads 「登录后分享」; update notice loses its box;
  更新链接 / 登录后更新 / K5 retry use the design's blue and retry icon.
- Signed-out observed share reuses the published link row and copy button
  (chain icon, green check 已复制) and no longer overflows its card.
- Capture specs follow the intended copy and placement changes.

* fix(share): keep the signed-out share panel instead of re-homing on sign-out

Signing out on a shared file page intermittently landed on Home instead of
the signed-out share panel (Owner-S13), depending on which re-read returned
first:

1. The workspace-context refresh re-adopted the route's bootstrap witness
   from the previous account, re-keying ProjectView on the same file.
2. FileWorkspace treated that remount as a file switch and cleared the
   observed public link.
3. With no link left, the tab scope reset fell back to Home.

- useProjectRouteWorkspaceContext: a bootstrap witness issued before an
  identity refresh is retired and never adopted after it (the hook's
  documented rule for resolved authority now covers the witness too).
- isObservedShareFileSwitch: mounting or remounting a file view is not a
  file switch; only a change to a different file clears the observed link.

Red specs: hook re-adoption (red on previous source) and FileWorkspace
remount (red on previous source). S13 capture: 8/8 passes, was 2/7.

* fix(share): align remaining owner share copy and K1 sync count with final design

- S0: signed-out description uses the design copy via its own key; the
  publish-failure message keeps its original text.
- S5: update notice reads 「文件有新改动,访问者看到的还是上一版。」.
- K2/K5: the failed-sync notice drops the 「分享已发布」 title and uses the
  design copy; terminal variants keep their own wording.
- K1: first-publication sync shows 「同步已有评论 · N 条」. The backfill
  state carries optional total/synced from the daemon's backfill members
  table, driving a determinate bar when progress is known; older daemons
  fall back to the previous copy and the indeterminate sweep.
- Refresh the PR screenshot of the Share popup comment-sync state.

* chore(web): restore generated next-env.d.ts accidentally included in 30361fd670

* refactor(share): give each owner share UI element one owning component

Behavior-preserving consolidation of the owner share surfaces, verified by
pixel diff of all 17 Owner capture specs against the pre-refactor baseline
(every state within the known 0-0.25% noise from timestamps, placeholder
animation and the dev badge; the share panel itself is identical).

- Shared components: LinkAccessRow, ShareNoticeRow, ShareProgressButton
  (S2 upload + K1 sync), ShareErrorRow, ShareButton (primary / soft /
  soft-error / dark-sm on @open-design/components Button), ShareSignInButton
  over a new useCloudSignIn hook shared with CloudSignInTip.
- The popover skin moves from a :has(.panel) override to an explicit
  .chrome-unified-popover--share class; SignedOutObservedShare reuses
  SharePanelHeader and the extracted rows.
- Icons: every inline share SVG moves into components/Icon.tsx with
  byte-identical path data.
- Tokens: styles/share-tokens.css holds the share palette and sizes as fixed
  light values (not app theme tokens, so dark mode renders as before); share
  CSS modules reference them. Single-use decorative values stay local.
- Delete the unreachable React-component share popover (canShareCurrentFile
  is false by design, Owner-G3) and its dead state and handlers.
- Toasts: share-adjacent feedback uses ShareFeedbackToast; top toasts share
  one stacking order instead of overlapping at top:64px.
- Static CSS-contract tests read share styles through readShareCss, which
  resolves share tokens to the design values they assert.

Allowed visual changes (existing defects):
- The sign-in entry is a real button; signing/error messages render below
  it instead of inside a nested button (UI-040).
- The comment side panel's retry-failed message is red (UI-042).
- Simultaneous top toasts stack instead of overlapping (UI-039).
- 「正在开启…」 goes through i18n (UI-041).

* feat(daemon): resolve share links only from the Viewer origin

Public share links come from OD_SHARE_VIEWER_URL(S) (HTTPS root origin)
as https://<viewer>/artifact/{projectId}/{slug}; the console origin is
no longer a fallback. GET publish-public derives the URL from the
resolver instead of the stored row, so pre-upgrade console URLs are
never echoed, and reports link unavailable plus the slug when no Viewer
origin is configured. Remotely stopped rows are not live. Manual-revoke
recovery data carries projectId and a nullable url. od project share
prints the slug in the unavailable state. Test fixtures move from
OD_VELA_WEB_URL to OD_SHARE_VIEWER_URL.

* feat(web): show a link-unavailable state for URL-less publications

A publication is published when it has a slug, not a URL. Without a
Viewer origin the share panels show a translated link-unavailable
status with Stop sharing, never the Publish CTA, so the file is not
uploaded twice. registry normalizes the publish and read responses to
{slug, url|null}; auto-copy is skipped without a URL.

* feat(tools-dev): inject a default share Viewer origin for local daemons

The default stays empty until the test Viewer domain is decided
(decision 67 #7), so local shares report link unavailable. An operator
OD_SHARE_VIEWER_URL(S) always overrides it.

* feat(daemon): parse C3-LITE member comment pages in both collab transports

Add a single-page member pull to the Vela CLI and HTTP collab clients
that returns either a typed page or a rebuild-required signal.

- The page envelope is validated strictly; malformed comments are
  skipped per item and reported, so one bad stored payload cannot
  block the cursor.
- Minimal tombstones ({id, seq, deleted}) are accepted and get the
  requested projectId.
- INVALID_CURSOR / CURSOR_STALE / SCOPE_CHANGED map to
  rebuild-required; INVALID_PAGE_REQUEST stays an error.
- Paged requests send no If-None-Match.

* feat(daemon): drain member comment pages into SQLite atomically

Team projects on the Vela CLI transport now receive member comments
through bounded C3-LITE pages instead of the unbounded legacy pull.

- Each page's merges, tombstone deletes and cursor advance commit in
  one immediate transaction; the persisted cursor is the cloud's
  stateless page/resume token.
- The drain continues through empty hasMore pages, hands a finished
  snapshot off to an incremental round, and stops at the end of an
  incremental round.
- A remote tombstone deletes a row that has a pin_seq; unknown ids
  are a no-op.
- A wake during a pull triggers exactly one trailing rerun.
- Merge notifications fire only for committed, changed rows.
- Share-page (user) comments stay on the legacy pull until public
  paging exists. The legacy pull also keeps carrying member comments
  whenever paging did not complete, and a cursor that needs a
  rebuild is latched until the rebuild path lands.

* feat(daemon): rebuild member comment cursors and isolate sync scopes

An unusable member cursor is no longer latched. It is reset and rebuilt
from a fresh snapshot in the same drain.

- A per-scope ledger records the ids the member stream delivered. The
  terminal snapshot page, in the same transaction as the handoff cursor,
  removes cloud-origin rows the snapshot no longer contains. Rows the
  stream never delivered (never-synced local comments, share-page
  comments, rows another account learned) are never pruned, and
  nothing is deleted before the snapshot completes.
- Writes committed during the snapshot arrive through the handoff
  incremental round and are not pruned.
- Scope is (workspace, member, team, project). A scope must prove it is
  current before taking a project over. Every page is re-checked after
  its await and before its commit (active scope, team-share binding,
  bound context), so an old account's or unshared project's in-flight
  page is discarded.
- Account switch and stop sharing drop that scope's cursor and ledger
  but keep the local comment rows as local copies.
- INVALID_PAGE_REQUEST and repeated transport failures back off per
  scope (bounded exponential, capped at 5 minutes). A refused rebuild
  snapshot backs off instead of looping.

* feat(daemon): announce comment merges once per pull round

A member page drain used to fire `onMerged` after every committed page,
and the legacy pull fired once more, so one 3-page drain of 250 comments
pushed three or four `comment-changed` events and cost the open web view
as many full list reads.

- Every row a round commits (member pages, prune, tombstones, legacy
  share-page rows) is summed into one `onMerged` call when the round
  settles, including rounds that pause, lose their scope or throw after
  some pages committed. A trailing rerun is its own round.
- A throwing listener is reported through `onError` and neither masks
  the round's own error nor turns a successful round into an unredeemed
  wake.

* feat(web): single-flight the comment list refresh and keep unchanged comments

Every comment trigger (project SSE, fallback poll, visibility, stream
reconnect, status patch) started its own full list read; the generation
guard only discarded stale answers. A burst of 10 `comment-changed`
events cost 10 reads, and a refresh that found nothing new still
published a new list and re-rendered.

- `refreshPreviewComments` goes through a single-flight runner keyed on
  project and conversation: a trigger during a read buys exactly one
  trailing read with the latest closure; a different conversation starts
  its own read and the old loop stops rerunning.
- Fresh reads are reconciled into the current state: unchanged comments
  keep their objects, an unchanged list keeps its array (no re-render),
  and attached comments follow the same rule. The initial conversation
  read reconciles too.
- The remote half of an explicit pull has a 15s deadline and falls back
  to the local list, so a stalled daemon pull cannot hold every later
  refresh behind it.

Measured in ProjectView tests (10 SSE events during one read): 10 -> 2
list reads; an equal-data refresh: 1 -> 0 new lists/renders.

* fix(daemon): make a published private team project team-visible before its comment backfill

Publishing a public link for a private project in a team workspace registers
it in the team catalog, but only the later background reconcile marked the
local row team-visible. Until then the row was personal in a team workspace,
which is neither Team- nor personal-relayable: the publish backfill was
cancelled (BACKFILL_DELIVERY_DISCARDED) and comments written right after
publishing were never queued.

- publish-public marks the creator's private row team-visible right after
  catalog registration, before the publication transaction enqueues the
  backfill (decision 67 #11); the response carries madeTeamVisible.
- The relay re-queues a personal record as Team relay once its creator's
  project turned team-visible, and defers (bounded) a personal record in a
  team workspace whose publication is still current instead of cancelling it.
- A cloud seq is never adopted as pin_seq when another confirmed row of the
  file already holds it, which produced two pins numbered 1.

* feat(share): tell the owner a public link makes a private team project team-visible

The share panel (and the React component viewer's share menu) shows
"After publishing, team members will be able to see this project" before
the first publish of a private project in a team workspace, in all 19
locales, and updates the scope control and team project views once the
daemon reports madeTeamVisible. `od project share publish` prints the same
notice to stderr and --json keeps the madeTeamVisible field.

* feat(packaged): hand the daemon its channel's share Viewer origin

Packaged desktop and headless daemons get the share Viewer origin for
their release channel: stable sends OD_SHARE_VIEWER_URLS with the prod
profile mapped to https://open-design.app; other channels send nothing
until the test Viewer domain is decided (decision 67 #10), so shares
report link unavailable. An explicit OD_SHARE_VIEWER_URL(S) at launch is
forwarded unchanged instead. The daemon resolver stays the only
validator. The legacy payload-desktop handoff strips both keys so a
channel default never becomes a sticky override.

* fix(daemon): report madeTeamVisible when a catalog reconcile marked the project first

Registering a private team-workspace project in the team catalog lets a
concurrent catalog reconcile (hub team-projects-changed, a project list
read) rebind the local row to team before publish-public marks it. The
mark then changed nothing and the response omitted madeTeamVisible, so the
share panel kept showing "only me" and the CLI printed no notice. Seen in
the local integration rerun on the first UI publish.

publish-public now reads whether the row is the creator's private team row
before registration, and reports madeTeamVisible when that row is the
creator's team row afterwards, whichever writer flipped it. A project that
was already team-visible still reports nothing.

* fix(daemon): order synced comment revisions by cloud seq so replays never undo newer state

A share-page comment resolved at cloud seq 10 reverted to open after a daemon
restart: the legacy pull cursor is in memory, so the restarted daemon replays
from seq 0, and the merge stamped Date.now() on the share-page create (which
carries no updatedAt), making the replayed create look newest.

Each preview_comments row now records the highest cloud revision seq it
reflects (cloud_revision_seq) and the seq space it came from
(cloud_revision_stream: relay team + member stream epoch). An incoming
revision at or below that seq in the same stream is a replay and never
overwrites the row. Unseen revisions keep updatedAt last-writer-wins when they
carry an updatedAt; one without updatedAt applies in seq order and is stored
with its createdAt, never the local clock. pin_seq is untouched.

* feat(share): take the canonical share link from AMR

The daemon no longer builds the public share address from a hardcoded or
per-channel Viewer origin. AMR reports it: `vela share publish|bind|resume|
project-status --json` carry `url` (https://<share origin>/artifact/{projectId}/{slug})
for an active binding, or `link: {status: "unavailable", code}`.

Link rule, one place (resolvePublicShareLink): an explicit
OD_SHARE_VIEWER_URL(S) override wins (a present invalid override is
unavailable, never a fallback); otherwise the AMR address, accepted only when
it is byte-for-byte the canonical HTTPS root-origin address of this project
and slug; otherwise unavailable, passing AMR's code
(PUBLIC_SHARE_WEB_URL_UNAVAILABLE / PUBLIC_SHARE_IDENTITY_INVALID) through.
A rejected address is logged without echoing it.

The verified AMR address is persisted with the publication and refreshed on
publish, foreground bind/resume, the background binding retry and every
publish-public status read, without rotating the publication witness. When
the status read fails, GET falls back to the persisted link marked
`stale: true` (not for a queued stop); a stored legacy console URL fails
re-verification and stays hidden until the next online read replaces it.

Packaged daemons no longer get a per-channel Viewer origin and tools-dev has
no default; an explicit launch override is still forwarded. `od project share`
prints the same link, a stale notice on stderr, and says when an alias has no
Viewer address.

* fix(share): keep the link access switch thumb at the left when off

* fix(share): settle the share status after a stop or resume in the open panel

* fix(comments): keep a comment that was handed to the agent

Sending a saved comment to the agent deleted it as soon as the send was
queued, so a run that failed or was stopped left nothing to retry and
pushed a tombstone to the share page.

The hand-off is now carried by status alone. The three send paths no
longer delete after queue acceptance; they only clear the composer and
the selection for the accepted ids. A run that fails, is reported
canceled, or is stopped by the user releases its comments back to open
through releaseCommentsAfterUnsuccessfulRun. A successful run still ends
in needs_review.

* fix(comments): keep an unsent comment draft when another element is picked

Picking another element, lassoing, opening a saved pin, or selecting a
comment in the side panel retargeted the composer at once and dropped
the draft, queued notes, and context without a word.

All four pick paths now retarget through requestComposerRetarget. While
the composer holds unsent work the first pick changes nothing and shows
a notice inside the composer; the next pick confirms the discard and
retargets. Editing the draft disarms the pending discard. Closing the
composer, Escape, and switching files behave as before.

* fix(daemon): walk nested HTML in share/deploy plans and refuse root index.html conflicts

Referenced .html/.htm files (iframes, frames) are now walked like the entry:
their references resolve against their own directory at any depth, so their
stylesheets, images and child pages ship and missing/invalid ones are
reported. Share mode rewrites their root-absolute references relative to
their own directory and maps references to the entry to the published
index.html; standalone deploy keeps their bytes.

Parsing is deduped on each file's real path, which bounds the walk even with
in-project symlink loops (x -> .) and also fixes the existing unbounded CSS
@import recursion through such links.

A reference to an existing root index.html while sharing another page no
longer overwrites the entry: the plan records the conflict (referrer file,
attribute, line, suggested free name) and deploy refuses it with 409
ENTRY_INDEX_CONFLICT carrying a copy-to-agent fix prompt.

* fix(daemon): refuse share entry/index.html conflicts before reserving

The share plan summary carries entry-index-conflict blockers (with the agent
prompt), and publish-public now builds the plan, and refuses on size or
blockers, before reserving an alias or preparing Vela. A conflict returns
409 SHARE_ENTRY_INDEX_CONFLICT with the details in error.data. Freshness is
not compared against a blocked plan.

* fix(daemon): print share blockers and agent prompt in the od CLI

od project share preflight prints each blocker with its copy-to-agent
instruction and exits 1 when blockers exist (also with --json). A publish
refused with SHARE_ENTRY_INDEX_CONFLICT prints the instruction readably in
text mode before the structured error envelope.

* fix(web): explain share index.html conflicts with a copy-to-agent action

The share panel shows the entry-index-conflict blocker from the share plan or
a 409 publish refusal, disables publishing while it stands, and copies the
daemon's agent prompt (falling back to showing it for manual copy). New
strings are translated in all 19 locales.

* fix(comments): release a comment whose queued send never becomes a run

A saved comment is written 'applying' when its send is queued, before any run exists. The release added for unsuccessful runs only fired on run end, cancel and stop, so a send held by the pre-run balance gate, stopped or failed before its run was created, or taken out of the queue left the comment 'applying' and out of the comment list for good.

Queue drains now go through startQueuedChatSend, which releases the comments of a send that ended without a run. A send the gate holds no longer reserves its comments, and removing or editing a queued send releases what it carried. A held send that starts later reserves them again when its run begins.

* fix(share): keep a reopened share busy until its link has been read

A resume answers without a link, so the panel re-reads the share state. The busy state ended with the resume response, and for the seconds the re-read took the panel said the link was temporarily unavailable before swapping to the link.

settlePublicShareAfterMutation now writes the active state only once the re-read has settled, and the publish flow stays busy on it. A re-read that fails or carries no link still ends on link unavailable, without an error.

* fix(web): size the copy-for-agent button to its longest label

The soft share button is a fixed 88px slot sized for short labels, so
"Copy fix for agent" overflowed in English and longer locales. Let the
button grow to fit, and stack both labels in one grid cell so the width
stays put while "Copied" shows.

* test(e2e): reopen a stopped share through the switch in the share audit specs

The share audit specs stopped a share inside the panel and then clicked
the publish button to bring it back, which is the behaviour the panel no
longer has: a stopped share reads as stopped and reopens through the link
access switch with a resume request.

The mocked publish responses also lacked the receipt the client parses,
so they now return the real response shape.

* test(share): align CI cases with current share UI

* test(share): align CI cases with current share UI

* test(web): pin comment time boundary to local noon

* fix(share): allow reopening saved comment after save

* fix(daemon): wallet 401/403 no longer expires the AMR session

Billing is display-only (OPEND-3553). Vela maps internal billing
failures to 403, so a wallet rejection now only reports the balance as
unavailable instead of marking the credential expired, which dropped
workspace context and redirected to sign-in.

* fix(daemon): billing summary failure no longer expires the AMR session

A failed 'vela billing summary' (even an auth-shaped 401) only drops the
optional plan/balance from /status. Credential expiry stays owned by the
non-billing workspace directory path (OPEND-3553).

* fix(daemon): billing projection 403 no longer flushes workspace authority

A rejected workspace billing snapshot only marks the billing runtime
entry. It no longer invalidates the workspace directory, exact authority,
or exact context caches with auth_reject; Vela also maps internal billing
failures to 403, and membership revocation stays owned by the directory
and the hub workspace-access-revoked event (OPEND-3553).

* fix(daemon): carry share content fingerprint across stop and resume

Resume reopens Vela's saved version of the same alias but writes a new
local publication revision, so the fingerprint row (keyed to the old
revision) made freshness permanently 'unknown' and hid the update entry.
Rebind the stored fingerprint to the resumed revision of the same slug so
later edits read as 'outdated' (OPEND-3517).

* fix(daemon): redact identity argv from vela command failures

Node's exec error repeats the full command line in message, cmd and
stack, and presence/member/catalog callers logged it verbatim, leaking
--display-name and activity payloads. The vela command runner now
rewrites the values of --display-name, --name, --email, --activity-json
and --metadata-json before rejecting (stderr and the upstream status
signal are untouched), and the presence failure log records only the
classification and upstream status (OPEND-3520).

* fix(daemon): refuse plain publish of a stopped share before uploading

When Vela reports the file's share as stopped, a plain publish-public POST
now answers 409 SHARE_STOPPED_RESUME_REQUIRED before pushing bytes, since
Vela rejects it with 409 share_binding_stopped anyway; reopening the link
is the explicit { mode: 'resume' } request. The share publish wrapper also
keeps the upstream 'API request failed with status NNN: code' line so the
failure classifies as upstream_http instead of reason=unknown (OPEND-3510).

* test(daemon): satisfy test typecheck for batch-2 share fixes

* fix(daemon): keep vela argv redaction from crashing on non-string args

Some media callers pass optional argv slots through as undefined. The
redaction must never replace the real command failure, so it skips
non-string entries (caught by vela-media-safety-process tests).

* fix(daemon): increase comment id entropy

* fix(share): keep deployment menu alive through pointer press (OPEND-3537)

* fix(share): unify owner Share entry glyphs (OPEND-3535)

* fix(share): show pending link-stop feedback (OPEND-3513)

* fix(share): retain scoped preflight across panel reopen (OPEND-3536)

* fix(comments): wait for preview targets before lost writeback (OPEND-3516)

* fix(share): restore Owner toolbar hierarchy (OPEND-3580)

* test(share): align toolbar style contract with C0 (OPEND-3580)

* fix(web): preserve owner scroll after comment location

* fix(cli): distinguish share size refusals from connection failures

* fix(web): separate share eligibility and preserve comment identity UX

* test(cli): preserve daemon connection failure exit code

* fix(share): expose owner-scoped persisted stop tasks

* fix(contracts): bound share anchors at original selector lengths

* fix(web): recover pending deleted-share stop notices

* fix(contracts): define atomic share pin snapshot metadata

* test(web): provide retry preflight fixture and await completion

* fix(web): keep billing upgrade outside account identity boundary

* fix(share): protect staging and restore client continuations (#8675)

* fix share staging and client continuations

* docs: clarify share batch verification baseline

* fix(web): notify when sending to a deleted project or conversation

* fix(share): dismiss card share popover on conversation scroll (OPEND-3784) (#8689)

* fix(share): keep anchored popover geometry current (OPEND-3783) (#8694)

* fix(share): expose freshness checks and parallelize state reads (OPEND-3791) (#8708)

* fix(share): preserve safe publish errors and actionable messages (OPEND-3796) (#8709)

* fix(share): preserve safe publish failure details and actionable messages

* test(share): align failure copy expectations and cover classified messages

* fix(share): retain Button binding required by CI merge

* fix(share): add bounded state-read and resume diagnostics (#8710)

* Revert "fix(share): expose freshness checks and parallelize state reads (OPEND-3791)" (#8711)

This reverts commit 8f6c09db39.

The reported latency bottleneck remains unproven. Revert the full PR at the user request and defer presentation optimization. Preserve the subsequent OPEND-3796 changes.

* fix(share): preserve entry binding checks when opening Share (OPEND-3594) (#8712)

* fix(share): report preflight failures and allow explicit retry (OPEND-3771) (#8716)

* fix(share): use green for update-link action (OPEND-3790) (#8717)

* fix(share): retain confirmed PDF export guide during history refresh (#8719)

* fix(analytics): add passive share and comment observations (#8723)

* fix(analytics): define passive share observations and contain measurement failures

* fix(analytics): observe share receipts and selected comments without changing actions

* fix(web): isolate after-export guide observation from actions

* fix(analytics): connect passive guide observations and document minimized fields

* fix(analytics): observe saved comment creation and run handoff passively

* fix(analytics): exclude edited composer drafts from saved-comment attribution

* fix(e2e): align Team run fixtures with authoritative workspace catalog

* fix(analytics): wait for comment acceptance readiness in regressions

* fix(share): 修复超限评论恢复与资源读取失败,补验四项验收问题 (#8721)

* fix(share): recover oversized comments and reject unreadable dependencies

* fix(share): close real export completion and cloud failure gaps

* test(share): align export and delete relay regression assertions

* fix(export): handle interrupted ZIP saves in preview actions

---------

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

* test(share): align export failure assertions with current copy

* fix(share): guard user-scoped export guide dismissal (OPEND-3579) (#8738)

* fix(share): guard user-scoped export guide dismissal (OPEND-3579)

* test(web): align export guide failure assertions with product copy

* test(web): settle restored share state before publish assertions

* fix(chat): keep artifact Share actions static (OPEND-3854) (#8747)

Keep ChatPanel artifact Share actions static and remove card-only publication state tracking. Preserve the existing share panel and readonly/generation gates.

* Merge pull request #8750 from nexu-io/fix/opend-3888-comment-age

fix(web): preserve comment creation age (OPEND-3888)

* fix(comments): persist new static comment identities (OPEND-3516) (#8768)

* fix(comments): preserve authored identities through manual and AI edits

* fix(comment): prepare durable static source identities (OPEND-3516)

* fix(comments): integrate durable identity lifecycle (OPEND-3516)

* fix(comment): validate authored HTML identity tokens (OPEND-3516)

* fix(comment): mask raw text before authored tokenization (OPEND-3516)

* fix(comments): enforce prepared document and publication barriers (OPEND-3516)

* fix(comments): revalidate pending documents and guard updates (OPEND-3516)

* fix(comments): require app-owned source preparation consent (OPEND-3516)

* fix(comments): contain preparation consent focus (OPEND-3516)

* fix(sharing): diagnose project bootstrap failures safely (OPEND-3516)

* fix(comments): retire stale publication guard alerts on explicit retry (OPEND-3516)

* fix(comments): locate Personal identities in the live preview (OPEND-3516)

* fix(comments): surface lost Owner identity anchors (OPEND-3516)

* fix(comments): unblock publication after strict target loss

* fix sharing to keep file publication independent of team visibility (OPEND-3855) (#8759)

* fix sharing to keep file publication independent of team visibility (OPEND-3855)

* refactor sharing to one explicit policy path (OPEND-3855)

* test: isolate metadata snapshots from the trailing identity directive

* fix: align collaboration consumers with explicit project visibility

Carry trusted catalog roots through content receivers and suppress stale Team rows after explicit privacy changes. Read shared design-system brand assets from their resource snapshots. Align stopped-publication ordering and protocol/browser fixtures with the existing contracts.

* test: await explicit comment preparation and independent UI states

Complete the existing source-version consent and real frame-readiness flow before opening the comment sidebar. Bind the response waiter to the UI actions so failures retain their real cause. Await the independently loaded account callout and assert exact catalog-bound materialization wiring.

* test: expect private policy when unsharing a team project

Await the explicit visibility-private command instead of deleting the retained root registration. Assert that remove was not invoked and preserve all member quarantine, directory and file-read privacy checks.

* fix(comments): synchronize file lifetimes across member and public shares (#8764)

* fix sharing to keep file publication independent of team visibility (OPEND-3855)

* fix(comments): synchronize current file lifetimes across members and shares

OPEND-3870 / OPEND-3871. Stack on published OPEND-3855 dependency; preserve reviewed source tree exactly. Local validation does not establish PostgreSQL or native acceptance.

* refactor sharing to one explicit policy path (OPEND-3855)

* fix(collab): adapt comment sync to converged project lifecycle

* test: isolate metadata snapshots from the trailing identity directive

* fix(collab): preserve nested publication and pulled file authority

* fix: align collaboration consumers with explicit project visibility

Carry trusted catalog roots through content receivers and suppress stale Team rows after explicit privacy changes. Read shared design-system brand assets from their resource snapshots. Align stopped-publication ordering and protocol/browser fixtures with the existing contracts.

* fix(collab): preserve relay identity and retire captured file batches atomically

* test: await explicit comment preparation and independent UI states

Complete the existing source-version consent and real frame-readiness flow before opening the comment sidebar. Bind the response waiter to the UI actions so failures retain their real cause. Await the independently loaded account callout and assert exact catalog-bound materialization wiring.

* test: expect private policy when unsharing a team project

Await the explicit visibility-private command instead of deleting the retained root registration. Assert that remove was not invoked and preserve all member quarantine, directory and file-read privacy checks.

* fix(collab): retain the actual native root during mirror materialization

* fix(collab): fail closed on unbound active share identity

* fix(collab): retire comment lifetimes after local mutation commits

* fix(collab): commit local artifact invalidation with working-root changes

* fix(collab): reconcile committed root retirement without stopping new links

* test(collab): align CI fixtures with admitted file identities and receipts

* test(collab): model receipt-backed root file admission in the shared hub

* test(collab): distinguish bound and unbound publication recovery

* chore(pack): pin compatible test CLI and exclude dependency dotenv files

* test(share): verify paired CLI policy and immutable share contracts

* fix(share): remove comment preparation interruptions

* fix(web): offer after-export guide when sharing history fails

* fix(comments): preserve unknown ages and repair synced creation times

* style(web): restore comment reference visuals without changing behavior

* test(web): verify colored avatars and document deployed parity

* fix(share): collect literal JavaScript image dependencies

* fix(web): match canvas comment pins to author avatar colors

* test(comments): align existing UI and E2E color expectations

* test(share): align collab flow and snapshot with direct comments UI

* test(campaign): await verification before advancing impression frames

* test(cms): await verification and mount before impression frames

* fix(web): match canvas comment initials to sidebar avatars

---------

Co-authored-by: PerishCode <perishcode@gmail.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: lefarcen <935902669@qq.com>
Co-authored-by: alchemist <alchemist@alchemistdeMacBook-Pro.local>
2026-10-19 06:15:28 +00:00
lorenzozaneandGitHub 78d5637cb4 fix(daemon,web): forward pure plugin applies and honor persisted capability grants (#7597)
* fix(daemon,web): forward pure plugin applies and honor persisted capability grants

Pure apply returns snapshotId "" for existing projects, so the web run
path now forwards pluginId and pluginInputs when no snapshot id exists.
The daemon trust vocabulary now accepts pipeline:* and applyPlugin honors
stored capabilitiesGranted with prompt:inject floor.

Fixes #6186

* test(web): cover pure-apply plugin identity fallback in run requests
2026-10-16 06:52:55 +00:00
dpeterson01andGitHub 86cb177228 allow custom model entry in the onboarding BYOK model picker (#6076)
* fix(web): allow custom model entry in onboarding BYOK dropdown

The onboarding wizard's BYOK model picker (OnboardingDropdown) always
showed the static SUGGESTED_MODELS_BY_PROTOCOL list with no way to type
an arbitrary model id, unlike the equivalent Settings dialog picker
(ByokModelField/SearchableModelSelect) which has a Custom sentinel
option. This blocked selecting any locally-installed model (e.g. an
Ollama model) that isn't in the hardcoded suggestion list.

Add an allowCustomValue prop to OnboardingDropdown: when enabled and
the search query doesn't match an existing option, a 'Use "<query>"'
entry is offered that sets the value directly. Enabled it for the BYOK
model field in OnboardingByokSetupPanel.

* fix(i18n): add settings.onboardingModelUseCustom to Dict and all locales

* add allowCustomValue tests; place custom entry last; single-quote CJK locale strings
2026-10-16 06:52:48 +00:00
lefarcenandGitHub 94f7a48633 fix(web): kick the compositor on a chat scroll freeze, then take the wheel and keyboard over by default (#8251)
* fix(web): kick the compositor on a chat scroll freeze, then take the wheel and keyboard over by default

When the freeze probe calls the chat log frozen, apply one frame of
`will-change: transform` so Blink rebuilds the scroller's compositing state
from the current layout, let the next downward wheel through natively and
measure it against what it asked for (the detector's own normalisation and
8px yardstick), and only if it stuck, fell short or snapped back answer the
wheel and the scrolling keys from JavaScript. The takeover is on by default
now; `open-design:chat-scroll-takeover = '0'` is the escape hatch.

`ChatPane` releases the takeover — and asks the probe to start a fresh
surface — when `activeConversationId` changes, because the log node is
reused across conversations. A healed surface is released the same way so a
recurrence can be judged again.

One new event, `client_chat_scroll_heal`, carries the outcome, the kick
geometry and the notch arithmetic, joined to the freeze by `probe_id`;
`client_chat_scroll_frozen` is untouched.

* fix(web): keep Home/End finite and let the takeover go when the chat log is removed

Home and End were ±Infinity folded into the same pixel accumulator as the
arrows, so Home-then-End in one frame (or key repeat) summed to NaN and the
browser read the NaN scrollTop as 0 — End could not reach the bottom. Edge
jumps now live in their own `pendingEdge` slot: the last edge pressed wins,
pressing one discards the steps queued before it, and steps pressed after it
apply on top; applyPending refuses to write a non-finite value.

A tab or route unmount removes the chat log without a conversation change,
and the probe only notices on a scroll the missing node never sends, so the
takeover stayed engaged with its document-level keydown listener cancelling
arrows and paging with nothing to scroll. Every input path (wheel, keydown,
frame) now checks `isConnected` and disengages, and ChatPane's release effect
also releases in its cleanup.
2026-10-16 06:14:33 +00:00
5a221d5043 fix(amr): refresh retained Home funding blocks after billing updates (#8786)
* fix(amr): invalidate Home funding hints on billing changes

* test(amr): stop replaying funding events on fixture reconnect

* fix(amr): retain confirmed Home funding blocks until recovery

* fix(amr): recheck blocked Home funding after billing refresh

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-10-16 03:28:03 +00:00
7abd6fc2b1 fix(amr): keep Home blocked on unknown funding confirmation (#8775)
* fix(amr): keep Home blocked on unknown funding confirmation

* test(web): await sign-in callout state independently

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-10-15 11:16:53 +00:00
ae321ab2e6 fix(cms): fence offline replay across revocation and process death (#8676)
* fix(cms): fence offline replay across revocation and process death

* feat(cms): define paginated Test catalog and immutable runtime context

* fix(cms): synchronize Test and Production host presentation lifecycles

* test(cms): retain cross-host lifecycle and browser acceptance regressions

* fix(cms): retain request tickets through body classification

---------

Co-authored-by: heggria <bshengtao@gmail.com>
Co-authored-by: ivy-tinggg <986810707@qq.com>
2026-10-15 07:37:41 +00:00
aa3a834046 fix(preview): refresh file snapshots after completed runs (#8744)
Co-authored-by: 罗高 <luogao@192.168.5.196>
2026-10-15 07:23:05 +00:00
535fa8eab7 fix(amr): restore Home funding checks and decouple run feedback (#8688)
* fix(web): surface live AMR funding failures without delaying sends

* fix(amr): check funding independently of agent startup

* chore(amr): pin Vela funding feedback test package

* fix(amr): route early funding hints through isolated CLI command

* fix(amr): restore scoped Home funding cache gate

* fix(amr): restore Home wallet and plan memory gate

* test(amr): align send audits with independent funding checks

* chore(amr): bundle Vela CLI 0.1.5 for funding checks

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-10-15 07:11:50 +00:00
ae8038e7ce fix(tabs): preserve saved local selections during restore (#8745)
Co-authored-by: 罗高 <luogao@192.168.5.196>
2026-10-15 04:36:01 +00:00
open-design-crew[bot]GitHubbone3deep1962-collabWilliamopen-design-crew[bot] <4197780+open-design-crew[bot]@users.noreply.github.com>NJUHuaAmy
14fee234e6 feat: add motion design workflow and curated motion skills (#8619)
* feat: add Motion Design skill and OD Next task route

* fix(web): rename motion design entry and place it second

* fix(web): reconcile motion design catalog consumers

* feat(web): add motion design templates to Home and Community

Five motion design example templates (3D procedural island, explainer,
product launch, showreel, data story), chosen by community reach, under the
Home motion-design chip and a new Community "Motion design" type.

Each template follows the bundled example layout: an OD-generated
example.html with its runtime libraries under assets/, an HTML preview baked
by the existing preview pipeline, and a SKILL.md that carries example.html
as the reference when the card is picked on the OD Next motion-design route.
Home cards ship curated copy for every UI locale. Motion templates stay off
the Video and HyperFrames chips.

* fix(skills): keep motion-design procedural layers visible in video export

The MP4 exporter seeks timelines with callbacks suppressed
(renderSeek(t, { suppressEvents: true })), so content drawn only from a GSAP
onUpdate callback is missing from exported video. Recommend a per-frame draw
loop that reads the timeline clock, and state that visible content must not
depend on timeline callbacks. The OD Next task asset is re-synced with
scripts/sync-motion-design-skill.ts.

* fix(motion): preserve reference assets and exported animation frames

* feat(skills): integrate curated motion helpers with complete runtime files

* feat(skills): add Huashu, Oil Motion and Motion.Lab adapters

* test: classify animate design guidance in narration guard

* feat(motion): refine five community templates and portable previews

* fix(motion): refine launch transitions and center gallery previews

* fix: allow free-form briefs on automatic motion task route

* feat(motion): add template soundtracks and audio export

* fix(motion): require rendered MP4 alongside editable video source

* test(motion): align strategy activation expectation with package version

* fix: scope motion delivery and make editable sources playable

* fix(motion): bundle source dependencies and valid closing protocol

* feat(motion): rebuild the five templates on the most-viewed community posts

Re-select topics by single-post views in the latest awesome-opus-5.5-video
snapshot and anchor each template to the top eligible post of its genre:
camera-focus explainer, showreel, product launch, procedural island and a
new wordless animated short that replaces the data story.

All five example.html files are hand-authored canvas/Three.js films drawn by
one render(t) from the GSAP master timeline, so preview, scrubbing and MP4
export produce identical frames. Each ships an original procedural
soundtrack, updated SKILL.md and 19-locale copy; Home order follows reach.

* fix(plugins): load bundled fonts in plugin previews

Plugin previews blocked every bundled font: the preview CSP had no font-src,
and relative url() references inside inline <style> blocks kept pointing at
the preview URL instead of the plugin folder. Allow same-origin fonts, map
inline-style url() references through the existing asset route (escapes,
absolute and data: URLs stay untouched), and serve font files with font
MIME types.

* feat(community): make motion template Use match the Home example pick

Community 「做同款」 on a motion design template now binds it as that type's
own example card, carrying the type's project metadata, so the run takes the
same OD Next motion-design route as a pick from Home's motion design list and
delivers the MP4 together with the editable source. Other task types keep
the existing plugin-pinning hand-off.

Community use clicks now record use_source (card | detail_modal) and
use_action (use | use_with_query); the details modal Use is tracked for the
first time. The Home hand-off no longer records a second community_gallery
click for uses Community already tracked.

* fix(plugins): count the five motion templates in the official registry metadata

* fix(media): keep HyperFrames audio filters compatible with FFmpeg 4.1

CI's bundled Linux FFmpeg 4.1 rejects adelay all=1 and amix normalize=0.
Make each track stereo and delay both channels explicitly, skip amix for a
single track, and undo amix averaging with a gain for several tracks.

* fix(motion): keep template previews bakeable on slow CI renderers

CI's software-GL baker skipped the 3D island (preview readiness timeout):
under slow frames GSAP lag smoothing slowed the timeline, so the 8 s
poster frame never arrived within the baker's 15 s wait. Play every
template on the wall clock and move the island poster to 6 s. With
simulated 300 ms frames all five now report ready within 10 s.

* feat(motion): add the kinetic typography template

Template selection now follows the motion design spec's four content types
(brand and product films, kinetic typography, explainer animation, procedural
art). Kinetic typography takes the fifth slot, replacing the animated short.
The new template is a 15-second, 120 BPM type film with an original
soundtrack and 19-locale copy.

* fix(motion): let the project motion player fit template stages

Inside a project the motion source player wraps the composition, fits it and
adds its own controls. The templates also scaled their stage, so editable
sources from Remix and Community use rendered at a fraction of the preview.
Templates now keep their own fit and controls only outside that player.

* feat(motion): restage the 3D island as a midday lagoon

The island moves from golden hour to a bright tropical midday closer to its
anchor post: a shader lagoon coloured by a baked depth map with ripples,
shallow-water shimmer and shore foam, a wide white-sand beach, palms and
instanced bushes, a village with a pier, soft sun shadows and a lower
camera path. The CPU wave mesh is gone, so frames cost no more than before.
Copy is updated in all 19 locales.

* fix(motion): keep each soundtrack inside the composition root

A Community use regenerated the island source tail and dropped the root
<audio> element, so its MP4 had no sound. Templates now place the element
inside the composition root next to the stage.

* docs(motion): state the template selection rule as complete works

* fix(motion): enforce video clocks and bound content verification

* feat(motion): replace the motion templates with remakes of viral posts

The five Motion design templates are now frame-faithful remakes of viral
motion posts, so each preview shows the look users came for: the 15-second
motion designer showreel, a high-energy motion reel (copy set in English), a
one-shape UI motion piece, a pixel-art wizard and a 20-second electric-blue
tech reel. Each ships open-licensed fonts, an original soundtrack, the
creator's published prompt where one exists, and credits its source post.

The explainer, product launch, 3D island and kinetic type templates are
removed. Tests follow the new set; a backward-seek check now covers hidden
WebGL layers.

* feat(motion): feature The Future Is Already Here first and start preview sound with the film

* chore: ensure PR CI

* fix(motion): match The Future Is Already Here controls and reorder the motion templates

* fix(motion): make the one-shape UI preview controls legible

* fix(motion): hide The Future Is Already Here controls inside the project player

* fix(motion): plan suitable sound and synchronize source audio playback

* feat(motion): keep remake soundtracks by default and fade exported audio where it stops

* test(motion): cover audible source controls in Chromium

* fix(test): use explicit ESM imports in audio player regression

* fix(motion): align preview track discovery with video export

* feat(motion): autoplay looping HTML without a player bar

* fix(test): consume the built motion playback contract

* docs(motion): state explicit soundtrack rules for make-same and the music line in replies

* chore(motion): version the audible looping delivery profile

* docs(motion): let template sound rules lead the general policy and freeze the audio contract

* fix(motion): scope autoplay to declared video motion sources

* fix(motion): reject audible MP4s under an explicit silent contract

* docs(motion): drop the routine music note from make-same replies

* test(motion): align delivery regressions with looping source playback

* fix(motion): isolate preview and audio export from legacy HyperFrames

* test(motion): retain the existing desktop legacy audio limitation

* fix(motion): suspend audio when previews leave the active workspace

* feat(motion): replace two motion templates with a product promo and a visual lesson; sign the showreel as Open Design

* feat(motion): give the showreel the Open Design palette and a full-size stacked wordmark

* feat(motion): give the product promo a livelier mascot girl

* refactor(motion): rebuild The Future Is Already Here as readable, editable scene code

* fix(motion): pick gallery cover frames that survive the baker's capture delay

* feat(motion): sharpen the showreel opener, depth, truchet and end card

* feat(motion): make the product promo mascot talk, add a speaking score and the flow pop

* feat(motion): give the visual lesson the original's paper-collage texture

* fix(motion): bundle gallery covers for fresh branch checkouts

* feat(motion): feature and refine Bring Ideas to Life

Adapt the paper story around OpenDesign capabilities, with short rounded hands, clearer timing, continuous paper actions, and a larger audience invitation. Feature it first in Motion and synchronize localized names, metadata, cover, guidance, and regressions.

Keep product promo audio music-only with sparse percussion.

* fix(motion): stabilize artifact cards and capture declared covers

* fix(web): share creation type order with community

* test(motion): exercise audio controls through project UI lifecycle

* fix(motion): scope live cover behavior to actual video sources

* test(motion): use real HEAD responses in cover regressions

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: William <zhouhaoran@refly.ai>
Co-authored-by: open-design-crew[bot] <4197780+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: Amy <1184569493@qq.com>
2026-10-14 15:55:52 +00:00
014bffbf77 fix(collab): synchronize owner devices with overwrite consent (#8683)
* fix(collab): synchronize owner devices with overwrite consent

* docs(collab): add owner device synchronization QA handoff

* chore(collab): move delivery reports and screenshots to Plane

* fix(collab): preserve owner pull fallback and ghost heal

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

* fix(collab): probe null owner heads for retractions

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

* fix(collab): preserve manual checks across coalescing

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

* feat(collab): hide update check behind a dismissed-version badge

Show the header entry only when the owner skipped a cloud version, as a
"Cloud update available" badge that re-opens the overwrite dialog.
Rewrite the dialog description in plain product language.

* fix(collab): clarify device sync copy and place entry before present

* fix(collab): silence update reminders without automatic overwrite

---------

Co-authored-by: Looper <looper@noreply.github.com>
Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
2026-10-13 11:14:14 +00:00
424f5c68cc fix(od-next): remove output-contract gates and unblock continuation (#8356)
* fix(od-next): continue plans with keyed markers and honor explicit deliverables

* fix(od-next): retire output contracts and hand off continued tasks

* fix(od-next): record round settlement and recover production cold

* fix(od-next): resume marker sessions with context deltas

* fix(od-next): separate completion facts and honor plan-only intent

* fix(od-next): preserve physical failures and legacy chat compatibility

* test(web): align run recovery fixtures and await campaign mount

* fix(od-next): tolerate terminal production marker formatting

* fix(od-next): unblock follow-ups after a stop or unreadable history (#8423)

Treat a previous task this build cannot read as absent so the follow-up starts a new task, and stop counting a Run with a pending cancel as holding its conversation. Add end-to-end coverage for the marker continuation flow and restore the remote Workspace directory gate cases.

* docs: clarify focused local validation

Keep local daemon and web validation scoped to affected files and integration boundaries. Reserve full suites for PR CI unless the user explicitly requests them.

* fix(web): scope folded task completion to its final run

Keep the final physical Run key and conclusion together when OD Next history is folded or accumulated live. Reuse the existing predecessor offsets while retaining the full display history and remaining Todo snapshot.

Add regression coverage for completed and unfinished production, live continuation, multiple Runs, legacy history, wrong and quoted markers, question handoff, and failed or canceled Runs.

Validated the parent regression before the fix, 165 focused Web tests, workspace typecheck, and guard.

* fix(od-next): compact historical context without restricting clarification

Reuse confirmed artifact summaries for old assistant history, preserve cross-agent plans and original requests, and represent earlier questionnaires as neutral historical information without inferring answers.

Keep user replies, skips, changed answers, unpersisted bodies, examples, and unfamiliar content intact. Do not add question repetition checks, output gates, repair rounds, or limits on clarification.

Validated red regressions, shared context and Web provider tests, focused handoff/resume/cold-production coverage, real server and fake CLI input, workspace typecheck, and guard. The repeated-question server fixture remains successful.

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: Cheems <hihicheems@gmail.com>
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
2026-10-13 10:32:45 +00:00
Ray XiandGitHub f29b1d6a02 fix(web): re-measure marquee travel when the document direction changes (#8692)
MarqueeLabel read the computed direction only when its text changed or a box
resized. I18nProvider writes <html dir> in a passive effect, so on first mount
in Arabic and on an English -> Arabic switch the label kept the LTR sign and
slid the workspace name the wrong way. Watch the dir attribute as well.

Refs OPEND-3112
2026-10-13 08:20:24 +00:00
Ray XiandGitHub 0bd8251c9b fix(daemon): name AMR runs failing on Windows older than 1809 as os_unsupported (#8629)
The bundled OpenCode is a Bun binary and Bun requires Windows 10 1809
(build 17763). On older builds it exits with STATUS_ENTRYPOINT_NOT_FOUND
(0xc0000139) before readiness, which was classified as a retryable
fatal_rpc_error and shown as a generic failure. Classify it as a
non-retryable os_unsupported when the host build is below 17763 and show
a card telling the user to update Windows.
2026-10-12 10:36:02 +00:00
af48a420eb fix(web): keep empty Home content reachable in short windows (#8649)
* fix(web): keep empty Home content reachable in short windows

Use safe centering when the empty Home hero exceeds the scroll viewport, and let wheel input continue from the examples list to Home at its boundaries.

Add CLI and BYOK browser regression coverage for short windows, responsive boundaries, scrolling, and centering when content fits. Validated with the focused Playwright cases, pnpm guard, and pnpm typecheck.

* test(web): align Home acceptance with safe centering

Update the existing empty-Home style acceptance to expect safe center while preserving its padding checks. The accepted layout change keeps content reachable when the hero exceeds the viewport.

Verified the CI assertion fails before the test update and all 8 cases in acceptance-visual-fixes.test.ts pass afterward. Also passed pnpm guard, pnpm typecheck, focused TypeScript diagnostics, and git diff --check.

* test(web): cover safe Home scroll boundaries

---------

Co-authored-by: PerishCode <perishcode@gmail.com>
2026-10-12 09:32:52 +00:00
Ray XiandGitHub c2a28e3cee fix(chat): clear the composer on submit instead of after the send gate settles (#8380)
* fix(chat): clear the composer on submit instead of after the send gate settles

The host paints the sent turn and flips to Working before the OpenDesign
Cloud balance preflight settles, but the composer only reset after onSend
resolved, so the sent prompt lingered in the input for the preflight round
trip. Snapshot the composer at submit, clear it immediately, and restore the
snapshot only when the host hands the send back ('restore-draft' or a
throw) and the user has not started typing a new prompt.

Refs OPEND-3392

* test(chat): type the deferred onSend mock so its call args typecheck

* fix(chat): skip restoring a handed-back send over any new composer content

Review follow-up: the restore guard only looked at editor text, so an
attachment, visual comment, skill, MCP server, connector, workspace
context, plugin or quote staged while the send was pending was
overwritten by the old snapshot. Mirror committed composer content in a
ref and restore only when nothing new appeared since reset().
2026-10-12 03:24:58 +00:00
802708f6c9 feat(web): land sign-in on Hosted and pitch free credits across entry points (#8567)
* feat(web): land sign-in on Hosted and pitch free credits across entry points

- Onboarding: drop the "Choose your model source" step. Signing in from the
  welcome screen finishes onboarding on OpenDesign Hosted; Local AI and API Key
  stay reachable from the welcome screen. Hosted select_runtime now reports
  step `connect` instead of `model_source`.
- Welcome screen: the pending sign-in shows one quiet "Sign-in page didn't
  open? Reopen" line (after 5s, or at once when the browser failed to open)
  instead of a nested card with two cancels, and the primary button no longer
  moves between idle and pending states.
- Home rail card: "Get free credits" pitch with a sparkle mark; the pending
  state turns the call to action into "Signing in…" with a single cancel and
  the same delayed reopen line. Dark mode keeps the button visible.
- Settings → Local CLI: callout reuses the free-credits copy; selecting a
  signed-out OpenDesign row selects it in place and offers Sign up / Sign in
  (no bounce to onboarding), committing the CLI once a session exists. Both
  buttons share the solid pill and show "Signing in… | Cancel".
- Fix the selected CLI card flashing on hover, and make the Home @ picker
  tabs clickable where they overlap the desktop window's drag strip.

* fix(web): drop pending OpenDesign pick when DeepSeek Harness setup commits

Installing DeepSeek Harness from its setup dialog committed agentId without
clearing amrPendingSelect, so the OpenDesign card stayed highlighted and a
later sign-in overwrote the DeepSeek choice with 'amr'. Clear the pending
intent alongside that commit and cover the sequence with a regression test.

---------

Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
2026-10-09 11:45:47 +00:00
lefarcenandGitHub 7eb1e217be fix(web): align client error copy with the product error-copy doc (OPEND-2849) (#8620)
* fix(web): align export and update-check error copy with product (OPEND-2849)

- S26a: a generic export failure now shows the product title + body
  ("导出失败" / "本次导出未完成,请重新尝试。") through the same Toast
  message + details shape as S26c. fileViewer.exportFailed is split into
  fileViewer.exportFailedTitle / fileViewer.exportFailedDescription in all
  19 locales; the old key is removed.
- S31b: drop the sentence-final period from the title line of
  updater.dialogCheckFailed (zh-CN and 17 other locales; th had none).

* fix(web): never surface raw exporter errors on export failure (OPEND-2849)

Product copy S26: an export failure shows the S26a title + body by default;
only an already-localized, user-facing reason may replace it. Raw exporter
strings (HTTP status lines, renderer/daemon errors, internal exceptions)
no longer reach the toast.

- New export-failure-toast module: LocalizedExportError marks errors whose
  message comes from an i18n key; exportFailureToast passes only those
  through and falls back to S26a for everything else.
- Localized reasons now thrown as LocalizedExportError: PPTX daemon
  unreachable / PPTX not available, version image capture failed.
- Image export catch-all uses the same rule instead of echoing err.message.
- The original error object is unchanged, so console.warn and
  exportErrorCode analytics still see the raw message.

* fix(web): align disk-write failure and browser-open hint copy with product (OPEND-2849)

- chat.runError.localStorageFailureMessage now matches the product supplement
  row 13 verbatim in zh-CN ("…请确认有足够的剩余空间后,再重新尝试。"); the
  old "permission to save to the current folder" clause is dropped and all
  19 locales are retranslated to the new meaning.
- settings.amrActivationBrowserFailed (S32 browser not opened): half-width
  comma replaced by a full-width one in zh-CN and zh-TW; wording unchanged.

* fix(web): align S03/S04/S06/S09a/S15/S24/S28a/S31c/S32a error copy with product (OPEND-2849)

zh-CN copied verbatim from the product error-copy doc; all 19 locales translated.

- S04 pre-send signed out (balance gate dialog): "Open Design 尚未登录" + body.
- S06 pre-send insufficient allowance: owner dialog "可用额度不足" + body (no
  balance figure); member dialog "团队额度不足" + body. Benefits list and
  buttons unchanged.
- S15 mid-run allowance exhausted: the turn-anchored upgrade card's $0 state
  shows title + body, personal vs. member chosen by the billing audience
  ProjectView already resolves. Low-balance tier, amount and Upgrade button
  unchanged; the pre-send tail card keeps "现在无法开始新任务".
- S24a unfinished todos: "任务尚未完成" + "本次任务仍有 {n} 项内容未完成。" with the
  real unfinished count; S24c user stop: "任务已停止" + body. Buttons kept.
- S03 / S28a Home send failures: the optimistic create's rollback toast (the
  surface users actually see) and HomeView's inline block now map
  AMR_AUTH_REQUIRED to S03 and transport failures to S28a, as title + body.
- S31c update download failed (updater error code download-failed): update
  dialog shows "更新下载未完成" + body; Settings status shows the title.
- S32a browser not opened during sign-in: title + body via one shared
  component for the three sign-in surfaces; the sign-in page link is kept.
- S09a rate-limit auto retry: run_retry_attempted now carries
  failure_category in the SSE contract; a rate_limit retry row reads
  "模型服务请求繁忙" + body, other retries keep "正在重试".
2026-10-09 10:00:10 +00:00
lefarcenandGitHub 426386581b docs(chat): record the OPEND-2807 error-card ruling (#8271)
The error card has shipped with a fixed set of three actions since #8140,
but that ruling was never written down: `git grep -i 2807 -- specs docs`
returns nothing. Two documents still describe the world before it.

`chat-panel-decisions-sheet.md` still reads "稿子 2 颗, 产品 4 颗" and
points at `run-error-catalog.md:421-430` for "A/B 两案仍挂着等产品挑".
Neither is true: the catalog's three candidates were all overruled, and
the card has three actions in both environments. `run-error-catalog.md`
§6.ZB still carries that candidate table as live, and §6.T still frames
the mapping as a ranking of secondary actions that no longer exists.

This records the ruling from main's own behaviour, not from the proposal
that lost: the criteria are quoted from `showCloudRetry` and
`showCloudSwitchCta` as they exist today, and the three exceptions
(a host that cannot take the handoff, `git_bash_missing`, a zero balance)
are the ones main actually ships. Historical passages stay; each one now
carries a pointer to the outcome.

The comment above `withCloudSwitchCta` pointed readers at `ChatPane`'s
`errorActionVariant`, a symbol #8140 deleted. It now describes the two
branches that exist.

No behaviour change: every `.ts` line in this diff is a comment.

Refs OPEND-2807
2026-10-09 03:28:31 +00:00
0520fde34b fix(chat): retain live thinking when older history arrives (#8167)
* fix(chat): retain live thinking when older history arrives

* test(web): cover full replay through the actual recovery host

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-10-09 03:28:26 +00:00
Pratik RaiandGitHub f86adccf3a fix(web): reveal full model names on hover in model pickers (#8409)
* fix(web): reveal full model names on hover in model pickers

Long model ids in the model pickers ellipsise to a shared prefix (e.g.
openrouter/google/gemini-...), so rows become indistinguishable and no title
was available to confirm a choice. Add the full label as the hover title on
every model-selection surface named in the report:

- the shared SearchableModelSelect option rows and trigger;
- the compact home list in InlineModelSwitcher;
- the composer/sidebar AvatarMenu lists (daemon and BYOK).

A locked row keeps its upgrade reason as its title, and accessible names are
unchanged. Adds regression tests using two same-prefix long OpenRouter ids on
each surface.

Fixes #8407

* chore(ci): retrigger web workspace tests after flaky artifact-tab shard

The ProjectView.auto-open-turn-artifacts spec timed out on shard 2/2; it
passes locally and is unrelated to the model-picker title change.
2026-10-08 07:36:52 +00:00
df2ebd6b57 fix(web): keep late memory cards in their source conversation (#8065)
* fix(web): keep late memory cards in their source conversation

* fix(web): preserve memory polling window ownership across retries

* fix(web): preserve late memory attribution across consecutive turns

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-10-08 05:55:15 +00:00
1bf9bf52f0 fix(projects): settle empty catalogs and expose list failures (#8443)
Co-authored-by: lefarcen <935902669@qq.com>
2026-10-08 05:28:22 +00:00
Ray XiandGitHub 3f83439dfe fix(chat): wait for successor run boundary before folding (OPEND-3463) (#8401) 2026-10-08 03:49:39 +00:00
e5d75aa8a1 fix(diagnostics): stop repeating API failures from flooding automatic uploads (#8461)
* fix(diagnostics): stop repeating API failures from flooding automatic uploads

Stable 0.24.1 devices uploaded thousands of diagnostic incidents a day from
one deterministic failure: GET /api/runs answering 400 PROJECT_SCOPE_REQUIRED.

- Key API-failure incidents by failure signature per hour instead of the
  per-request id, so a polling caller yields one incident per hour.
- Drop earlier "[diagnostics] incident delivered" receipt lines from
  automatic bundles; on busy devices they filled the daemon log tail.
- Desktop pet task center: stop interval polling once the daemon requires a
  project scope, and stop restarting the poller on every 1.5s config reload.

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

* fix(diagnostics): filter receipts before allocating the log tail budget

Scan automatic text logs backward with bounded buffers, preserving complete lines and consent offsets before applying the retained byte limit. Cover receipt suffixes larger than 4 MiB and filtered-tail boundary cases.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 06:56:27 +00:00
Ray XiandGitHub 512f302d40 fix(web): retire stale recent-project status polling (#8442) 2026-09-28 06:56:12 +00:00
1b47e60bd4 fix(cms): mount Test campaigns with the requested app locale (#8432)
Co-authored-by: lefarcen <935902669@qq.com>
2026-09-24 06:58:26 +00:00
eb27acd322 feat(billing): support Coding Plan usage and wallet fallback (#8339)
* feat(billing): support Coding Plan usage and wallet fallback

* fix(web): keep AMR send off billing preflight

Workspace send no longer waits on advisory vela billing preflight; recovery still requests it. Realign zero-wallet P0 oracles and the credits-card wallet label.

Generated-By: looper 0.15.0 (runner=fixer, agent=omp)

* fix(billing): serialize recovery preflight and bound hung vela

Skip overlapping AmrBalanceDialog recovery ticks and stop the watch after
ten minutes even if a preflight is still pending. Give vela billing
preflight a 60s runVelaCommand timeout so a hung CLI cannot pin recovery.

Generated-By: looper 0.15.0 (runner=fixer, agent=omp)

* fix(web): restore coding plan text contrast in billing card

* feat(web): render Coding Plan windows and Go tier in the credits panel

- CodingPlanUsage renders one row per preflight window (name, used %,
  remaining USD, reset countdown + absolute time) with loading/unavailable
  states; refreshes once on mount and once at resetsAt (no 30s polling,
  setTimeout delay clamped to the 32-bit limit).
- Pure model in coding-plan-usage-model.ts; creditsPerUsd comes from
  `vela billing summary` through WorkspaceBillingSummary and falls back to
  10,000 only when the backend omits it.
- PlanWordmark/EntryNavRail accept the `go` tier (placeholder wordmark,
  design asset still pending).
- i18n: 7 new billing keys + entry.billingTierGo across all 19 locales;
  real translations for the #8339 keys; drop unused codingPlanNone /
  codingPlanRemaining.
- CLI wording: unstarted window reads "not started yet".

* build(pack): pin vela-cli 0.1.4-test.0 and gate on billing preflight

The packaged client needs `vela billing preflight` for the Coding Plan
window rows. Bump the bundled CLI to the npm `test` prerelease that ships
it and add a third capability gate in tools-pack: a CLI without the
subcommand prints the parent `billing` help and exits 0, so the gate
checks the help markers (preflight, --workspace-id, --model, --format)
instead of the exit code.

TEMPORARY: replace 0.1.4-test.0 with the stable 0.1.4 once it is
released; do not merge with a `test` prerelease pinned.

* fix(web): make Coding Plan rows legible on the inverted credits card

The credits card is a hard-coded #202020/#fafafa inverted surface, but
CodingPlanUsage.module.css read page-level --text-* / --bg-fill-* tokens,
so on the card the window names rendered #202020 on #202020 (1.00:1) and
the stat/reset lines sat below WCAG AA. Derive every colour from
currentColor instead (name 100%, stat 72%, reset/note 60%, separator ~50%,
track 36%, exhausted = --red mixed toward currentColor); measured in Chrome:
name 15.6:1, stat 8.7:1, reset 6.4:1, track 3.2:1, exhausted 6.5:1.

Keep the stat on one line (nowrap) and let the name ellipsize instead of
wrapping "60% / used" onto two lines; drop the block's 12px side padding so
it aligns with the card's own rows.

Also add a minutes tier to the reset countdown: the last hour before a
window resets used to show no countdown at all. New key
billing.codingPlanResetsInMinutes in all 19 locales.

* feat(web): rebuild the plan panel 1:1 from design PR #8364

Replace the improvised Coding Plan block with the design's panel, measured
against docs/ui-previews/plan-panels/index.html in Chrome (computed styles
and a 4x pixel diff of the Plus specimen):

- Header: tier name + 16px wordmark, 升级 pill (管理 on the top tier).
- One row 「7天额度免费用 · 已用 N% ›」 (7-day window only; rounded percent;
  100% when spent, no red state) over a 5px #b1f38b track, then the
  separator and the 「钱包余额 · US$x.xx ›」 row. No headings, window names,
  remaining USD, reset time, note or unavailable line.
- Team workspaces keep the pre-existing card unchanged (all team_* tiers);
  the quota row renders only for personal workspaces.
- Free personal workspace: no quota row, but the header sells 升级 like the
  design (a personal workspace needs no billing permission; an account B
  reports without a plan upgrades as free).
- Top-right pill shows the wordmark only, no balance.
- Wallet always reads US$ regardless of locale (product ruling 2026-09-23).
- zh-CN tier names follow the design: 免费版 / 标准版 / 专业版 / 旗舰版.
- Drop the dead billing.codingPlan* keys and model helpers; add
  billing.codingPlanWeeklyAllowance and entry.creditsManage in 19 locales.

* feat(web): show 管理 on personal Max per design PR #8364

The design's Max card sells 管理 (console auto-recharge settings) where
every other tier sells 升级; product ruled on 2026-09-23 to follow the
design for personal Max too, superseding the earlier "personal tiers are
always upgradeable" ruling. The gate now asks isMaxPlanTier and 管理 wins
over 升级 when both would show.

* feat(web): route the plan card's three entries per product ruling

2026-09-23 ruling on the card's landings: 升级 opens the console's pricing
dialog (billing=plan, unchanged), the allowance row opens the console
dashboard (unchanged), the wallet row asks for the manual top-up dialog
(new billing=recharge intent; B does not honor it yet and degrades to the
dashboard), and 管理 on Max opens the plain dashboard instead of the
auto-recharge dialog.

* fix(web): align personal plan cards with v2 design and scoped loading

* fix(web): center plan wordmarks inside the top-right pill

* fix(web): reuse shared billing cache for plan hover cards

* test(web): match personal billing preflight in avatar fixture

* fix(billing): require usable quota evidence for funding recovery

* test(billing): cover scoped preflight and quota event refresh end to end

* feat(daemon): relay scoped coding plan usage events and health

* fix(billing): refresh quota caches from scoped realtime events

* test(daemon): accept capability query on mock event stream

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-09-24 03:22:31 +00:00
5885573546 feat(web): localize welcome screen and starter credit ribbon (#8351)
Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
2026-09-24 03:13:16 +00:00
PerishFireandGitHub 7bad1a45f2 feat(telemetry): classify publish and deploy failures additively (#8402)
Publish-public and deploy failures were only visible as one opaque bucket
each (`publish_failed`, generic HTTP_400 / Error), so neither PostHog nor the
automatic diagnostics bundles could tell why they failed.

Purely additive — no existing status, error code, event value, trigger or
copy changes:

- daemon: failed publish/unpublish/deploy responses gain an optional
  closed-token `failure` { stage, reason, upstreamStatus?, upstreamCode? }.
  Vela CLI failures are classified from the CLI's stable stderr contract
  (vela-cli 0.0.33–0.1.3); deploy failures separate provider rejection,
  unreachable provider, rejected Vercel token, local file and internal
  errors. Each failure also writes one structured, text-free log line.
- web: artifact_publish_result / artifact_deploy_result gain optional
  failed_stage, failure_reason, upstream_status, upstream_error_code, and
  (publish) daemon_error_code. `error_code` keeps its values.
- correlation: each attempt's analytics request id is sent as
  x-od-request-id, logged by the daemon, and reported as request_id.
2026-09-23 12:19:20 +00:00
Ray XiandGitHub 282416ccd8 feat(diagnostics): automatically collect consented failure evidence (OPEND-3397) (#8357)
* feat(diagnostics): persist and upload consented Agent fault evidence

* feat(diagnostics): capture cancellation and delivery experience failures

* feat(diagnostics): collect visible frontend failures through consented relay

* fix(web): reset cached analytics before diagnostic test mocks

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

* fix(web): reset template picker before it becomes interactive

* test(web): await campaign presentation before retry polling
2026-09-23 11:12:53 +00:00
Ray XiandGitHub 055621c906 fix(web): keep the task-type menu open when the click follows a type change (#8386)
TemplatePicker closed its menu from a passive effect keyed on activeChipId and
disabled. On a loaded machine React runs a commit's passive effects in a later
task, so a click on a trigger that already showed the new type opened the
menu and the stale effect closed it again. Close in the render that
introduces the change instead.

Home also enabled the picker one render before its first-visit default type
(or restored draft / handoff) landed, so a click in that gap opened a menu the
arriving type then closed. Keep the picker disabled until the initial type is
settled.
2026-09-23 09:38:12 +00:00
06e0fd7259 fix(cms): keep a campaign on screen through network, sleep and clock noise (#8292)
* fix(web): page recovery no longer withdraws a valid touchpoint lease (OPEND-3363)

`online`, `pageshow` and `visibilitychange` were all bound to the destructive
`wake()`, which calls `revoke()` synchronously — before the revalidation it
starts has produced any evidence. Switching Wi-Fi, waking from sleep or tabbing
back therefore unmounted a campaign the server had authorized, and the user saw
it vanish and (sometimes) reappear.

The loss could also be permanent. An emptied lease makes `abandonAttempt`'s
`!lease.current` branch true, so the first failed revalidation dropped the saved
`revalidationLease` too; the retry that succeeded came back `{kind:"retain"}`
with nothing left to restore. The impression was already recorded, so the
activity never showed again for that session.

The three recovery events now take the non-destructive shape `focus` already
had, named `resume`: a lease still inside the window the server granted keeps
its mount and revalidates in the background, a lapsed lease falls through to
`wake`, and a hidden page cancels only the request in flight. `armExpiry` still
retires every lease on the server's own deadline, and a 410 withdrawal still
closes at once.

Deliberate behavior-contract change. Three existing cases pinned the old
destructive semantics and are rewritten here with the opposite outcome:

- `withdraws old authority synchronously on wake and rejects a timed-out
  revalidation` → `keeps an unexpired visible decision mounted while an online
  revalidation is pending`
- `still withdraws display and authority on actual page hiding` → `fences a
  hidden page without withdrawing the lease it was granted`
- `fences the old modal action until recovery mounts a fresh decision` → `keeps
  the mounted modal action authorized across focus and online recovery`

The badge case additionally drops from three refreshes to two: `online` now
joins the revalidation `focus` already has in flight instead of tearing it down
and re-requesting.

Every new case asserts inside the pending revalidation window, not on the
settled state — the blind spot the four network edge cases this week shared.

* feat(web): let a production touchpoint hold the authority the server grants (OPEND-3366)

`resolveAuthorizationDeadline` takes the minimum of the server's
authorization, the activity's `endsAt` and a local cap. With A3 removing the
sixty-second clamp on `authorizationExpiresAt`, that local cap became the
binding term: a five-minute `MAX_LEASE_MS` truncated every long authorization
back to five minutes, so a client that lost the network went blank in the
middle of an activity that was still running.

The cap now lives once, as `PRODUCTION_MAX_LEASE_MS`, at the longest interval a
timer can name — a backstop against a server clock that grants past the
activity, not a policy. `endsAt` governs, which is what the server means.
`armExpiry` already segments the wait with `Math.min(remaining, MAX_TIMER_MS)`
and is unchanged.

All three production placements (Modal, Badge, Hover) had their own copy of the
five-minute constant, which is exactly how two of them could keep a short lease
after the third was fixed. They now share the one exported value, and a test
reads all three sources to keep it that way.

Depends on OPEND-3363: before it, page recovery emptied the lease before any
failure was evaluated, so a long authorization had nothing to protect.

* feat(daemon): assemble touchpoint content locally instead of re-downloading it (OPEND-3371)

A production decision refresh re-downloaded the whole content package every 30
seconds, although the bytes almost never change. B4 lets a caller say which
content it already holds and get a `contentOmitted` reply, but taking that
benefit in the browser would need a new desktop package — every already-shipped
prerelease would keep paying full price. Putting it in the daemon means the
client does not change at all and existing installs benefit immediately.

The daemon now carries `heldContentId` / `heldContentLocale` on its own behalf,
and puts the omitted `content` back from a local store before answering the
browser. Two layers: blobs addressed by their own digest, so the four
placements of one activity share `shared.js` / `theme.css` once; and one
assembly record per (placementKey, locale) naming the manifest, the entry and
the path → digest map. The root is derived from `RUNTIME_DATA_DIR`, so two
namespaces get two stores and neither can see the other's.

The proxy could stay a pure `source.pipe(dest)` only as long as nobody had to
read the body; this route now buffers and rebuilds the JSON. Everything else —
the Test runtime, the events endpoint, any production request the daemon holds
no content for — keeps the verbatim streaming path.

Two properties carry the design, and each has a named case:

- A daemon holding nothing sends today's request and returns today's response
  byte for byte, with no `contentOmitted` anywhere. Published clients guard on
  `if (!next.content?.id) return clear`, so breaking this makes campaigns
  disappear.
- Nothing behind the daemon may cost the campaign. A miss, a corrupted blob, a
  digest that stopped describing its bytes, an unwritable data directory, a
  Vela that ignores the parameters — each falls back to exactly today's full
  request. A trimmed reply that cannot be rebuilt is re-asked once, without
  held content, and that retry can never trim again.

The cache holds no opinion about WHICH activity to show: the server re-decides
that every request, so a stale cache cannot pin a withdrawn activity, and a new
one is still visible within one poll.

Measured on a realistic package (real minified JS/CSS as content bytes):
638.8 KB raw / 190.1 KB gzip for the full response against 0.7 KB raw /
0.4 KB gzip for the trimmed one — the steady-state WAN body is under a
kilobyte per refresh regardless of how large the activity is.

* test(web): pin what a rotating touchpoint decision id costs the client (OPEND-3369)

The server-side measurement can only show that `touchpointDecisionId` differs
per request (120 distinct ids in an hour of polling). Whether that costs
anything is decided on the client, in the lease key: `refresh` bumps
`generation` whenever the key changes, and the mount effects are keyed on
`generation`, so a rotating id is a full teardown and re-mount per poll.

Every existing lifecycle case uses `key: "same"`, so this path had never been
observed. Two pairs of cases observe it now, at both seams:

- `useTouchpointLifecycle`: a rotating key over 120 polls produces 120
  generation bumps and a new decision object each time; the same hour with a
  stable key produces zero.
- `ProductionCampaignModal`: the same hour produces 121 distinct
  `opend-touchpoint` elements and 121 `mount` calls with a rotating id, against
  one element and one mount with a stable one.

Tests only; no behavior change. This confirms the client-side benefit claimed
for B3 — 120 remounts an hour become none once the id is stable.

* fix(web): take the one-shot credential out of the touchpoint lease key (OPEND-3374)

All three production placements put `touchpointDecisionId` in their lease key. A
changed key is `++generation`, which is a full teardown: shadow DOM rebuilt,
Blob URLs re-created, entry animation replayed, scroll lock released and
re-taken. But that id is not content identity — it is a credential the server
re-issues whenever its own sixty-second row lapses.

OPEND-3369 stabilised the id and removed the every-thirty-seconds version of
this churn. It did not remove the cause. The credential lives 60s and the client
polls every 30s, so missing two polls — a Wi-Fi switch, a tunnel, a closed lid —
expires the row; on recovery the server INSERTs a new one and the campaign
flickers. That moved the remount from "on a timer, for everyone" to "whenever
the network wobbles", which is the same population the recovery-lifecycle P1 was
about.

The key now answers only the question it exists to answer — same content, same
person, same deployment — through one shared `touchpointContentIdentity`, so the
three placements cannot drift apart.

Hover needed arguing rather than deleting, and the argument came out differently
than expected. Its key carried the ENTRY's content id plus BOTH credentials; the
layer's content id was never in it, so the layer's identity rode on its
credential. Whether a deployment always gives both placements the same
`content.id` is a server-side property this side cannot verify, and this repo's
own hover fixtures assume it does not (each placement gets its own version). So
the layer's content version is now named explicitly. The red test for it —
`still remounts the pair when only the layer's content version changes` — fails
on the previous key by showing the layer still rendering the replaced content.

Retaining the previous decision object means the client presents a credential
the server has expired. Both uses of it are asserted here rather than assumed:
`still clears on a 410 whose receipt echoes the stale decision id the client
kept` covers OPEND-3372, and `reports the stale decision id on a click after the
credential rotated` covers OPEND-3364. Without those two, this change would
become a revocation failure or lost attribution.

Deliberate contract change. The three OPEND-3369 observation cases measured the
cost of a rotating id and are rewritten with the opposite outcome:

- `rebuilds the mounted host on every poll when only the decision id changes` →
  `keeps one mounted host for a whole hour of rotating decision ids` (121 hosts
  and 121 mounts become 1 and 1)
- `keeps one mounted host for a whole hour when the decision id is stable` keeps
  its result and loses its role as a contrast — the two now agree, which is the
  point
- the hook-level `remounts once per poll when the decision id rotates ...` is
  re-framed as `... when the lease key rotates ...`: the hook never knew about
  decision ids, and which inputs change a key is now decided in the placements

OPEND-3369 is not undone — its write-side saving stands on its own and this
returns it to being purely that.

* fix(web): judge a set-aside lease by its own window, not by an empty screen (OPEND-3376)

**The ticket asked for `revalidationLease` to be deleted as unreachable. It is
not unreachable, so this takes the alternative the ticket names instead: fix
`abandonAttempt`'s criterion. See below — this needs a decision.**

The reachability argument was: `wake()` has one call site, `resume`'s `else`
branch, which runs only when the lease is missing or lapsed; so `wake()` can
only ever set aside a lease that is already expired; and the restore branch
requires the set-aside lease to still be valid; so the two are mutually
exclusive.

Every step is true except the last. It needs "expired" to be a property that,
once true, stays true. It is not. `elapsed()` is `max(monotonic, wall)`, so a
wall clock that steps FORWARD makes a live lease read as expired — `resume`
then takes the `wake` branch — and a correction BACK makes it read as live
again. NTP steps, VM resume and dual-boot clocks all do this, and this module
already takes wall-clock skew seriously enough to have a case for it.

Verified with a throwaway probe against this branch's HEAD: step the clock an
hour forward, dispatch `online`, step it back, resolve `{kind:"retain"}` — the
restore branch fires and the campaign comes back. Deleting the mechanism would
therefore have been a silent behavior change, not a no-op.

So the P1 hazard is repaired where it actually lives. `abandonAttempt` decided
whether a failure may end display by asking whether there is an ACTIVE lease,
treating "none" as "expired". `wake` empties `lease.current` before it
revalidates, so the next failure met that branch, spent the set-aside lease too,
and the retry that finally succeeded came back `{kind:"retain"}` with nothing
left to restore — gone for the session. The criterion now judges whichever lease
is still recoverable by its own window.

This is stronger than the deletion would have been. The deletion's safety rested
on `wake()` keeping exactly one call site, which nothing enforced; adding one
back would have reopened the loss. After this, a new `wake()` call site cannot
cause permanent loss at all, because the short-circuit that caused it is gone.

Red first: `no single failure can discard display authority that is still
recoverable` drives the clock excursion, fails one revalidation inside the
set-aside lease's own window, and asserts the retry restores display. It fails
on HEAD and passes here. All 42 pre-existing lifecycle assertions are untouched
and still pass, so nothing was bent to fit.

What is NOT delivered: `revalidationLease` still exists, and so does the
three-state lease machine the ticket wanted reduced to two. Whether to keep the
restore branch (it un-does a spurious withdrawal caused by a bad clock) or drop
it and accept one remount plus up to a 30s gap in that case is a product call,
not one to make inside a "delete dead code" ticket.

* refactor(web): a lease carries content identity, not authorization timing (OPEND-3377)

Since OPEND-3374 a matching key retains the previous decision object, which is
the point — consumers depend on `decision`, so a new reference would re-mount.
But the retained object is the whole response DTO, and three of its fields
describe something that does not survive being retained: `serverTime`, `endsAt`
and `authorizationExpiresAt` are whatever some earlier response happened to
carry. How long display may last comes from the lease's own `validForMs`, taken
fresh every time.

Nothing reads the stale copies today. That is a fact about who has written the
consumers so far, not about the code: the day someone adds `decision.endsAt` to
a countdown they read an end time the operator has already moved, and nothing
fails to tell them. Same species as OPEND-3376 — correct, but resting on an
invariant nothing enforces.

Option (a) from the ticket. `touchpointLeaseValue` drops the three fields and
`TouchpointLeaseValue<T>` says so in the type, and the value really does not
carry them, so a consumer that casts past the type still finds nothing. One
named helper, three placements, matching `touchpointContentIdentity` and
`PRODUCTION_MAX_LEASE_MS`. (b) was rejected because `never`-typed fields still
read as present and still invite `as`.

Chosen for the reason the ticket gives: it states the boundary rather than
patching a symptom. A lease carries a STABLE content identity, which decides
whether to re-mount, and a FRESH authorization window, which decides how long
display may last; these are different things and no longer share a DTO.

The type system then found the rest by itself. `dispatchProductionCampaignAction`
declared it took a whole `Decision` while reading only static actions and
identity — it already receives the window separately as `expiresAt` — and
Hover's `matches` did the same. Both are narrowed; no call site changed.

Evidence for the acceptance bar: adding
`console.log("activity ends at", decision.endsAt)` to the Modal's mount effect
produces

  src/components/ProductionCampaignModal.tsx(375,46): error TS2339:
  Property 'endsAt' does not exist on type 'AuthorizedDecision'.

Three type-level assertions in the suite keep that true without needing a human
to re-run the experiment: each `@ts-expect-error` is itself checked, so if a
field came back TypeScript would report the directive as unused and `pnpm
typecheck` would fail.

Pure narrowing. Every pre-existing assertion in the lifecycle suite and in all
three placement suites is untouched and still passes, including OPEND-3374's
retained-credential cases, and the web suite is green.

* test(web): pin that a receiptless 410 withdraws display (OPEND-3375)

Vela now answers a request carrying an `activeDecisionId` for a deployment that
has been taken down with `410 production_runtime_withdrawn` and no receipt,
shaped byte for byte like a rollout withdrawal that cannot name one. The client
half of that was explicitly outside its authorization.

The client already handles it correctly, and this locks that in rather than
changing it: the receipt parse is what decides, so any 410 that does not yield
all four fields becomes `http_410`, and `http_410` is the one detail that sets
`touchpointWithdrawal` — the single flag `withdrawsDisplay` reads, and the only
failure permitted to end a lease the server already granted. Every other failure
is transport noise the lease rides out, which since OPEND-3363 it really does
ride out, so a 410 quietly reclassified as noise would now mean a withdrawn
campaign stays on screen for the rest of its lease.

Two cases, and neither goes red first — they are regression locks, not fixes:

- the loader, over the withdrawal body, the rollout body it mimics, a null
  receipt and an empty body, asserting `http_410` + `touchpointWithdrawal`
- the Modal end to end through the real loader and lifecycle: a withdrawn
  deployment closes the dialog on the next poll, and a later 200 does not bring
  it back

* fix(web): a clock step must not re-mount a campaign that never left the screen (OPEND-3378)

Audit of every consumer of `elapsed()` under the non-monotonicity found in
OPEND-3376: `max(monotonic, wall)` rises and falls again when a wall clock steps
FORWARD and is corrected BACK. Ten call sites; nine acceptable, one not. The
verdicts are in the report; the reasoning that survives in code is on `elapsed`
itself, whose old one-line comment ("a forward jump can only shorten it") is
what made the shape sound monotonic in the first place.

The one that had to change is `same`, which decides renewal against re-mount.
It asked whether the previous lease was still inside its window, and a forward
step makes a perfectly live lease answer no. The response arriving during the
step is then a new presentation: `++generation`, rebuilt host, rebuilt Blob
URLs, replayed entry animation — the flicker OPEND-3374 removed, re-entering
through the clock. A3 made leases run for the whole activity, so the window a
step can land in went from a minute to days.

It is also not defensible on its own terms. Nothing evaluates `elapsed` until
something asks, so a step alone tears nothing down (verified by probe): the
campaign is still mounted and still authorized when the poll lands, and there is
no withdrawal for that re-mount to correspond to.

So the question is asked only where it means something: a lease being RESUMED
from `revalidationLease` must still prove it is inside its window, because its
display really was withdrawn. A lease that is still `lease.current` renews. A
mounted lease that has genuinely lapsed is not reachable here — `armExpiry`
retires it, which empties `lease.current`.

Red first: `a clock step forward does not re-mount a campaign that is still on
screen`. It fails on HEAD with `expected 3 to be 2` — one generation too many.

Deliberately NOT changed: `elapsed` itself. The monotonic term stops a clock set
back from granting time; the wall term stops a sleeping device from freezing the
lease, because `performance.now()` pauses across sleep on some platforms. Each
closes a cheat the other does not, so making it purely monotonic or purely
wall-clock would be a regression, and the fix belongs at the call sites that
care about the direction of the error.

Every pre-existing assertion is untouched, including OPEND-3376's
`no single failure can discard display authority that is still recoverable`.

* fix(web): drop the visibility fence whose premise OPEND-3363 removed

#8269 and OPEND-3363 are each correct and together reproduce the P1 they were
both written to remove: a campaign that vanishes on a tab switch.

#8269 added a `visibilitychange` fence releasing the open presentation whenever
the page goes hidden. Its own comment states the premise — "A hidden page ...
withdraws the lease and takes this modal down with it" — which was true when it
was written. OPEND-3363 removed exactly that: hiding now cancels only the
request in flight and leaves the lease, and the modal, alone.

So the fence released a presentation that had not ended. The campaign stayed on
screen with nothing recorded as presenting it, and the poll arriving on return
took `!continuesOpenPresentation && wasDisplayed(...)` with
`openPresentation.current` already null — the `{kind: "clear"}` branch — and
closed the host.

Reproduced before touching anything, on the rebased branch:

  keeps a displayed campaign through a tab switch now that hiding no longer
  withdraws it
  → TestingLibraryElementError: Unable to find an accessible element with the
    role "dialog"

and the failure is on RETURN, not on hide: the modal survives being hidden, so
this is the interaction and not a regression in either change alone.

The fence is removed. What it protected is still protected, by the presentation's
own deadline: it is anchored to the authorization that opened it, so a sleep long
enough to lapse the lease also lapses the presentation, and the offer arriving on
wake is correctly read as a new one. That is asserted rather than argued.

Everything else from #8269 is kept, including the parts this branch was behind
on: campaign hosts confined to the home view, `onError` releasing the
presentation only for a real withdrawal, the `revokesActive` refactor, and
`touchpointWithdrawsDisplay`.

Deliberate contract change. #8269's `does not re-present a displayed campaign
after the page is hidden and shown again` asserted that hiding takes the modal
down, which OPEND-3363 made false. It is rewritten as `does not re-present a
displayed campaign after a sleep outlasts its authorization` — the property it
was actually protecting — with a sleep long enough to be a real one.

* fix(daemon): repair a damaged content blob instead of keeping it forever

`remember` skipped any blob file that already existed. The name is the
digest, so a present file "should" hold exactly those bytes -- but a file
damaged after it was written keeps its name, and `remember` is the only
moment the daemon ever holds the correct bytes for it again. Every other
round it is answering from a trimmed reply that carries no content at all.

The result was a closed loop with no exit and no signal: `held` sees the
files exist and offers the version, Vela trims its reply to ~1KB,
reassembly fails on the digest, the fallback refetches the full ~270KB,
and `remember` declines to overwrite. That placement then pays a trimmed
ask plus a full ask on every single poll -- strictly worse than running
with no cache at all -- and nothing in this module logs or counts it.

Writing unconditionally costs nothing in steady state, because `remember`
only runs on a response that was not trimmed. The rewrite is atomic and
idempotent, so the loop now closes after exactly one round.

The existing HTTP case pinned the defect as expected behaviour: it ran a
single round and asserted two upstream calls. It now runs one round
further and asserts the second round is back to one conditional ask.

* fix(daemon): make a blob's digest a statement about the file itself

`readVerifiedBlob` hashed a decode of the stored file and then returned
the file. Those are not the same bytes. Node's base64 decoder drops every
character outside the alphabet -- anywhere in the input, not only after
the padding -- so anything appended to or injected into a stored blob was
invisible to the digest while still being handed back and forwarded to
the browser.

The handover notes called this harmless on the grounds that the browser
decodes the same way. It does not. `atob` is WHATWG forgiving-base64: it
strips ASCII whitespace and throws `InvalidCharacterError` on anything
else. So the daemon reports a verified package, skips the fallback it
keeps for exactly this case, and the browser refuses to mount it. With
the blob-repair fix this placement would still self-heal in one round;
before that fix the pair was permanent, silent, and invisible to every
integrity check on both sides.

Blobs now store the bytes their digest names and are re-encoded to base64
on the way out, so `sha256(file) === digest` is an exact claim about
everything the cache returns. Modules already worked this way; the two
layers now share one invariant.

Two consequences, both deliberate:

  - blobs written by an earlier build no longer match their digest. They
    are repaired on the next full response, which is why the blob-repair
    fix has to land first -- without it every one of them would be
    stranded and cost double forever.
  - a rebuilt response now carries Node's canonical base64. It stays
    byte-identical to the original only while upstream sends canonical
    base64 too, which Vela does today. The warm/cold equality case is the
    assertion that will fail if that ever stops being true.

The cache fixture's shared resource was exactly 24 bytes, so its base64
form had no padding and appended garbage folded into the decode and broke
the digest by accident -- the new cases would have passed against the old
code for the wrong reason. It is 25 bytes now.

* fix(daemon): bind reassembly to the version it was negotiated for

`reassemble` took only the cache key, so it reread the record from disk
and rebuilt from whatever that record said at that moment. The record is
mutable: a concurrent full response for the same placement replaces it.
When it did, the daemon spliced the new campaign's content into a reply
the server had framed for the old one -- and nothing downstream could
tell. The browser verifies that the content is internally consistent, and
it is; both halves are. So it renders campaign Y's creative and reports
its impressions against campaign X's decision id. Billing and attribution
cross silently, on a path where every integrity check passes.

The pair this attempt offered upstream is now an argument, and
`recordStillHolds` is the invariant: a reply trimmed against one version
may only ever be rebuilt against that version. A record that has moved on
returns null, which the existing fallback already handles by re-asking in
full.

The locale it compares is the one the daemon OFFERED, not the one it
asked for. Vela resolves a placement locale through [requested, base
language, en-US], so a zh-CN request is legitimately answered and cached
as en-US, and `held` reports that faithfully. Comparing the requested
locale would strand every placement the server answers through a
fallback, which is why a case now pins that too.

Dropping the upstream request on abort ships with it, because the two are
one defect in practice. This proxy had no cancellation at all, while the
AMR proxy a few hundred lines up has had it all along. The browser's
per-attempt budget is 15s and this proxy's is 30s, so every attempt the
browser gives up on stayed alive here for up to fifteen more seconds:
still downloading, still buffering, and still writing the assembly record
that a later, live request was about to rebuild from. `res.headersSent`
does not catch it, because an attempt aborted before a byte went out has
sent no headers. That is what turns the race above from a one-round-trip
window into a fifteen-second one, which is why fixing only the first half
would leave reviewers with the wrong idea of how wide it is.

The fake Vela can now be taken over by a case that needs to control when
a reply lands, not only what it says. It still may not bypass the server's
own rule -- content is omitted only for a caller that already holds
exactly the version being served -- so the crossing above is produced the
way production would produce it: by promoting a campaign mid-flight.

* fix(daemon): put a ceiling back on buffering and decompression

Before content assembly this route forwarded production decisions as a
stream, so its memory was bounded by the socket. Reading the body to
assemble it gave that up and replaced it with nothing: `size` was tracked
only to size the final `Buffer.concat`, and the three `zlib.*Sync` calls
ran without `maxOutputLength`, which leaves the limit at
`buffer.kMaxLength` -- effectively none. Measured on this Node: a 199KB
gzip response allocates 200MB in 51ms, a ratio of about 1000:1. The 2MiB
check people point at runs four allocations later, inside the cache, long
after the damage is done.

Two things make this worse than it looks for a daemon. The decompressors
are synchronous, so an oversized body does not merely consume memory, it
holds the event loop of a privileged local process while it expands. And
this is not an edge route: it is the steady-state poll every placement
makes twice a minute.

Both limits now derive from one number. `MAX_CONTENT_BYTES` -- the budget
the web host already enforces on a content package -- is exported, and
the proxy allows four times it, which covers base64's 4/3 inflation plus
the manifest and metadata wrapped around the resources. A body past that
cannot be assembled into anything the browser would accept, so buffering
it buys nothing.

Past the ceiling the route degrades to streaming rather than refusing:
the caller still gets its answer, in one WAN fetch, framed exactly as
upstream framed it. That is the behaviour this route had before, and it
is what the pre-existing `passThrough` path does for every other request.

* chore(daemon): escape the key separator so the file is not binary to git

`keyName` separated the placement key from the locale with a literal NUL
byte written straight into the template string. Git classifies a file
containing NUL as binary, so this one showed up in the pull request as
`+0/-0` and `Binary files differ`. It is the 11KB module the whole
content-assembly feature lives in, and no reviewer on GitHub could open
it -- which is exactly where the defects fixed earlier in this branch
were sitting.

`\u0000` is the same character. Nothing about the hash, the file names on
disk or any stored record changes; only git's guess about the file type
does.

* fix(web): stop the timer limit binding the production lease again

`PRODUCTION_MAX_LEASE_MS` exists to catch a server clock that grants past
the activity it is granting for. Giving it `MAX_TIMER_MS` answered a
different question -- what one `setTimeout` can name -- and so made it the
binding term in `resolveAuthorizationDeadline` for every schedule longer
than ~24.9 days.

That is the same defect OPEND-3366 was written to remove, one tier up. The
five-minute value truncated every longer authorization to five minutes;
`MAX_TIMER_MS` truncates every schedule longer than ~24.9 days. `armExpiry`
has segmented long waits since this landed, so the timer's reach was never
the lease's problem to borrow.

It is reachable. The server validates only `endsAt > startsAt` and a future
`startsAt` -- there is no maximum schedule length anywhere in the write
path -- and its own production-runtime fixtures assert multi-year windows
are handed back unshortened. A device that misses every poll for 24.9 days
(a lid closed over a long trip, then opened offline) wakes to a truncated
lease, `wake` treats the offer as a new presentation, and the recorded
device impression retires it. That campaign never shows on that device
again.

The docblock made the same mistake in prose, and said so confidently: "At
the longest interval a timer can name it stops binding, and `endsAt`
governs". True below 24.9 days, false above it, and it is the sentence that
would have let the next reader repeat this. Rewritten to keep the two
bounds apart -- one timer segment versus one authorization -- and to record
both times the constant was given the wrong one.

Ten years, not `Infinity` and not a dropped term: skew is exactly what
`endsAt` cannot catch, because `validForMs` is `deadline - serverTime` and
`endsAt` is what the lag is measured against. Not one year either, for the
same reason five minutes failed -- the team's own fixtures schedule past it.

* test(web): give the sleep case a control that can tell absence apart

Both assertions in "does not re-present a displayed campaign after a sleep
outlasts its authorization" are absences. An absence is also what a
lifecycle that never came back looks like, so the case could not
distinguish the device impression closing a live offer from no offer
arriving at all -- and a `wake` that stopped waking would have left it
fully green.

Verified by construction: with `resume`'s lapsed-lease branch stubbed to
return instead of calling `wake`, both `toBeNull()` assertions still pass
and only the new control fails.

The control drops the impression and wakes once more through the same
event and the same advance, so the impression is the only difference
between presenting and not.

* test(web): cover the two background-recovery cases A5 had no test for

A5 (screen sleep / backgrounding) was carried as unverified because
`document.hidden` cannot be forced inside a real Electron renderer. That is
true of Electron and not of this suite, which has been forging it all along
with `vi.spyOn(document, "hidden", "get")`. Two of its four cases had no
coverage at all.

Short side, at the modal. The existing tab-switch case toggles visibility
on the real clock with nothing in between, so it cannot tell "the lease was
left alone" from "nothing had time to lapse". The new case is hidden for
forty-five seconds of a sixty-second authorization: a poll tick falls
inside the spell, and the lease is still the server's to renew on return.
It asserts the same dialog node, the same `opend-touchpoint` host, and zero
requests while hidden.

Long side, at the hook. Nothing covered a lease lapsing WHILE the page is
hidden. `armExpiry` does not consult `document.hidden`, so the grant
retires on the server's deadline whether anyone is watching or not, and the
return has nothing left to revalidate: it must go through `wake`, exactly
once. `generation` is asserted as changed rather than as a number, because
this path steps it three times and pinning the count would pin the route
instead of the outcome.

Both were checked by breaking what they claim to hold:

  - visibilitychange bound back to `wake` (pre-OPEND-3363) -> the modal case
    fails on the first assertion after hiding.
  - `refresh` no longer fencing a hidden page -> the modal case fails on the
    zero-requests assertion.
  - `armExpiry` consulting `document.hidden` -> the hook case fails on
    `current` being null.
  - `resume` no longer waking a lapsed lease -> the hook case fails on the
    single `load`.

Recorded for the next reader: reverting OPEND-3378 does NOT red the modal
case. The lease is still inside its window at the moment of return, so
`same` holds either way; 3378 needs a clock step forward, which is already
covered by "a clock step forward does not re-mount a campaign that is still
on screen".

What is left for a human is one platform fact, not product logic: whether
an Electron renderer flips `document.hidden` on sleep, occlusion and
minimise at all. Recovery does not depend on that event firing -- the
expiry timer ignores it, the thirty-second poll runs regardless, and focus,
online and pageshow share the same handler -- so the worst case is one poll
interval of delay rather than a wrong branch.

* docs(web): scope the OPEND-3377 stripping claim to production

"They do not survive into the lease" reads as a property of
`useTouchpointLifecycle`. It is a property of `touchpointLeaseValue`, and
the hook has a fourth consumer that does not apply it: `TestCampaignModal`
retains whole decisions with `serverTime`, `endsAt` and
`authorizationExpiresAt` intact, and reads across polls from the object it
retained.

That is the right call there -- the Test channel exists to show an operator
what the schedule is doing, its authorization is capped at sixty seconds
and every round recomputes from a fresh `serverTime`, so nothing it holds
can be stale by more than one poll -- and this change does not touch it.

What it does is stop the note reading as a repository-wide invariant when
the source scan behind it only visits the three production files. Left
unqualified, the next reader adding a consumer would believe a guarantee
nothing is enforcing.

* test(web): stop waiting for a real animation frame on a fake clock

The device impression is recorded inside a `requestAnimationFrame`, and
`requestAnimationFrame` was not in either case's `toFake` list. So the frame
stayed on the REAL clock while the case advanced only the fake one, and
`expect(localStorage.getItem(marker())).toBe("1")` after a fixed
`advanceTimersByTimeAsync(16)` was really asking whether enough WALL time
had happened to pass inside the awaits. On an idle machine it had; under
load it had not.

Measured, 12 busy cores against one file, six runs per arm:

  - "keeps the same host through a background spell" (mine)      3/8 red
  - "does not re-present ... after a sleep outlasts"             3/8 red
    with every one of my commits reverted -- this one arrived with
    the #8269 rewrite and was already failing before I touched the file
  - after this change                                            0/29 red

Both now go through one helper that advances in small steps until the
marker lands. Each step yields to the real microtask queue, which is what
lets `verifyWebTouchpoint`'s real crypto finish; the frame it then requests
fires on fake time. Ablation, same load: the stepping alone is what carries
it (6/6 green); faking frames alone is strictly worse than before (6/6 red),
because a single fixed advance can run out before the frame is even asked
for. Frames are faked anyway, so the mechanism the case waits on belongs to
the clock the case controls, instead of to the wall.

"keeps the displayed campaign on screen when a poll fails and its retry
recovers" -- the case CI reported, and the A1 watchman from #8269 -- is not
touched. It sets the marker by hand precisely because of this, and it was
green in all 14 runs with my commits reverted. It has a weaker form of the
same fragility, noted for a follow-up: it reads
`document.querySelector("opend-touchpoint")`, which the host satisfies
synchronously, so nothing in it waits for the mount that sets
`openPresentation`. Starve that crypto long enough and the recovering poll
finds no open presentation, reads the impression it set by hand, and clears
the host. That is how it can fail, and it is not this PR's to change.

* fix(web): stop bounding production display authority by a lease duration

`resolveAuthorizationDeadline` took `serverTime + PRODUCTION_MAX_LEASE_MS` in
its minimum, so the ten-year value truncated every longer grant. The server's
own production-runtime fixtures run 2020-01-01 to 2100-01-01, so the premise
that no operator's schedule could reach it was false when it was written:
after ten years offline the client would withdraw a campaign the server still
authorizes, which is OPEND-3366 again at a larger threshold.

The term is removed rather than widened. Three times it was filled with a
number that answered a different question -- five minutes (the poll interval),
`MAX_TIMER_MS` (one timer's reach), ten years (a guess at "far enough") -- and
each became the binding answer to this one. Every fill was defended as a skew
backstop, but a duration cap only ever sees the SUM of the skew and the
schedule, so no value can bound one without binding the other: a value small
enough to catch a month of skew truncates every multi-year campaign. There is
no fourth value to try.

Skew is carried instead by the terms that survive it. `authorizationExpiresAt`
stays in the minimum, so a clock-derived credential window cancels the offset
exactly; `endsAt <= serverTime` still refuses an activity already over on the
server's clock; and a successful poll replaces `validForMs` outright, so an
inflated window is only ever spent by a client that cannot reach the server
for the whole of it. The residue -- a server clock wrong by a month
over-displays by a month, an error the size of the skew and not of the lease
-- is recorded as accepted, together with why comparing `serverTime` against
the device's `Date.now()` is not the fix.

The one maximum left is the Test runtime's sixty-second contract, now named
`TEST_MAX_AUTHORIZATION_MS` and a rejection threshold only; it cannot shorten
anything. The regression matrix gains the server fixture's own 2100-01-01
window, and the three-placement guard now asserts production passes no
maximum at all.

* test(web): wait for the mount behind the impression, not for a hand-written marker

`keeps the displayed campaign on screen when a poll fails and its retry
recovers` wrote the device-impression marker itself, to get past a frame its
`toFake` list had left on the real clock. That manufactures a state the
product cannot produce: `openPresentation` is assigned when `mountTouchpoint`
resolves and the marker only in the frame after it, so "impression recorded,
nothing open" exists in the case and nowhere else -- and it is exactly the
`{kind:"clear"}` branch of the load callback. Whenever the real SHA-256 behind
the mount had not finished inside the case's fixed ten-millisecond advance,
the recovering poll therefore tore down the very host the case then asserted
on, which is the `expected null not to be null` this went red with.

This is an existing weakness in the case, not one this branch introduced.
Starving the digest reproduces it identically on 1c4751266d, before the
lease-cap change; and the value that change removed never bound this fixture,
whose `authorizationExpiresAt` of `serverTime + 60s` was the minimum's
binding term under the old ten-year cap and stays so without it.

The case now fakes frames through `IMPRESSION_TIMERS` and waits through
`advanceToRecordedImpression`, like the two siblings beside it. That wait is a
real mount barrier, because nothing can write the marker until the
presentation is open. Nothing is relaxed to get there: this case guards the
blocking defect from the OPEND-3363 acceptance report, and reverting the
`onError` guard it exists to protect still turns it red.

`advanceToRecordedImpression` also stops spending fake time on a wait that is
really about the real queue. A step used to buy exactly one turn of it, so a
machine slow at the crypto ran out of steps while fake milliseconds were still
plentiful; each step now yields the real queue before advancing its frame.
Against a deliberately starved digest the case used to break at forty turns
and now survives four hundred, and when it does give up it names the wait that
failed instead of leaving an unexplained null host behind.

* feat(cms): keep a cached activity on screen while the runtime is unreachable

OPEND-3436. A client that cannot reach the CMS runtime went blank sixty
seconds later — the length of the authorization window it happened to be
holding — and then spent the outage retrying a request that could not
succeed. Restarting lost the activity entirely, because the daemon's
content store held the bytes of a package but nothing about WHEN it was
authorized to show them.

The daemon now owns persistence and the clock. Its assembly records carry
the server's own startsAt/endsAt/serverTime/authorizationExpiresAt, the
activity/deployment/content identity, and the decision envelope, scoped by
(environment, account) so one account's packages are unreachable from
another's session. When the runtime cannot be reached it rebuilds the last
decision it stored, re-times it, and answers 200 with `offlineReplay` — the
same body the browser already parses, with two fields rewritten:
`serverTime` becomes the daemon's estimate of now, and
`authorizationExpiresAt` becomes `endsAt`. That second rewrite is the
ticket's product ruling: offline display lasts to the last known END of the
activity, not to the short authorization it was issued.

Elapsed time is measured so it can only increase — an in-process monotonic
reading, and a persisted high-water mark that survives a restart — so
winding the system clock back cannot buy display time. A record whose
window has closed is deleted on the next read, along with the bytes no
surviving record still references; the blob pool is shared by digest, so
reclaiming without asking the survivors first would break every other
activity built from the same component. A 410 deletes only when all four
fields of its receipt name this record. A 404, a timeout and a 5xx delete
nothing.

The browser reads one field and changes two behaviours: a failure that
means the runtime was not reached suspends the poll and the backoff chain,
and recovery becomes one deduplicated revalidation per reconnection —
however many of online/focus/pageshow/visibilitychange that fires. The
window still ends on its own timer with no network involved, and a lease
that lapsed while the device slept is retired before anything is
revalidated, so waking does not flash an activity that is over.

The opt-in is off by default. The Test channel shares this hook, its job is
to keep asking, and there is no cached content behind it to fall back on.

Scope note: three existing regression tests recovered from an outage on the
next poll tick and now dispatch the `online` event a real reconnection
fires. What they guard — no remount across the outage — is unchanged.

* fix(web): keep asking while a 5xx holds the offline fallback

OPEND-3436 stands the poll down on the first failure that means the
runtime was not reached, and leaves recovery to `online`, `focus`,
`pageshow` and `visibilitychange`. That bargain holds for exactly one of
the two failures it covers.

When the device's own network broke, its repair fires `online`. When the
server answered 5xx, nothing broke at the device end at all: the request
crossed a healthy network and came back with an answer, `navigator.onLine`
stayed true throughout, and a user who simply leaves the app open on the
page the activity appears on fires none of the other three either. No
event anywhere says the server is healthy again, so a client that went
quiet on a 5xx stays quiet for the whole cached window while fully online.

What that costs is the ticket's own product bargain. A revocation is only
delivered in the answer to a request this client makes, so a client that
has stopped asking cannot be told an activity was pulled -- it keeps
showing it. A shortened schedule and a newly published activity are lost
the same way.

So a fallback held by a 5xx now keeps one request every
SERVER_FAULT_HEARTBEAT_MS. The interval is fixed rather than backed off
because its job is not "recover early" but "bound how late a withdrawal
can land": the longest a pulled activity can stay up is the longest this
client will go without asking, and only a fixed number answers the
question an operator actually asks. It goes through `revalidateOnce`, so
a heartbeat landing beside a reconnection is still one request, and a
heartbeat that fails starts no retry chain -- it just waits for the next
bound. One success returns it to the ordinary 30s poll.

A transport failure gets none of this, and a red guard pins that: a
device with no network answers every request the same way and its
recovery is already announced.

* test(web): wait for the menu the click schedules, not the tree before it

`pickHomeTemplate` read `home-hero-template-menu` with a synchronous
`getByTestId` on the statement after `fireEvent.click`. The click only
schedules the open state; the menu is not in the DOM until React commits,
so the helper passed only while that render happened to win the tick.

On `main` alone it wins every time. Merged with this branch — which mounts
the campaign placements into HomeView — the same tick carries more work and
the read starts losing: measured 0/6 failures on `main`, 1/6 on the merge
that CI actually builds, surfacing as `HomeView.example-dismiss` failing on
a testid that file never mentions.

`findByTestId` waits for the commit the click asked for. The read is what
was wrong, not the timing it happened to get.

* fix(web): arm the heartbeat on the path production actually takes

The heartbeat added in the previous commit could not fire in production.
It armed from `abandonAttempt`, which only runs when a request REJECTS,
and an unreachable runtime never reaches this browser as a rejection: the
daemon absorbs it, rebuilds the decision from its own cache and answers
HTTP 200 with an `offlineReplay` marker. The loader then collapsed that
marker to `offline: true`, so the lifecycle stood its poll down and armed
nothing. Every heartbeat test passed by mocking a rejection the real
stack does not produce.

That left the exact hole the heartbeat was written to close, and left it
on the likelier path. Both of the daemon's reasons -- `upstream_unavailable`
for a runtime 5xx, `upstream_unreachable` for its own DNS, connect or
timeout failure -- happen entirely behind the daemon, over a
browser-to-daemon connection that never broke. `navigator.onLine` stays
true so `online` cannot fire, and a user sitting on the page fires no
`focus`, `pageshow` or `visibilitychange` either. With no request on a
clock, a withdrawn activity stays on screen until the replayed `endsAt`,
which the daemon re-times to the whole remaining window and which a real
schedule may set years out.

So the marker is carried up whole instead of flattened, and the lifecycle
takes a recovery POLICY rather than a flag: "stop asking" and "nothing
will tell you when to start again" are different facts, and merging them
is what hid this. `productionTouchpointRecovery` maps reason to policy in
one exhaustive switch, so a third reason added upstream cannot inherit an
answer nobody ruled on.

The guard is unchanged and still green: a failure of the browser's own
transport is announced by `online` and gets no heartbeat.

Pinned by an integration case that drives the real loader -- daemon 200
replay in, 410 withdrawal out, and not one browser event in between.

* fix(daemon): decide what a status licenses before degrading to streaming

A decision body that outgrows MAX_BUFFERED_DECISION_BYTES makes the proxy
give up assembly and become a pipe. Becoming a pipe sends headers, and
sending headers retires the status block in the `end` handler — so the
size of a body was silently deciding what its status meant.

Two consequences, both reproduced by the specs added here:

  - an oversized 410 never called `forgetWithdrawn`, so the browser cleared
    the screen while the stored package survived its own withdrawal and
    stayed eligible to replay the next time the runtime was unreachable;
  - an oversized 5xx was forwarded instead of answered from cache, which is
    the outage this route exists to survive.

Neither decision needs the bytes. `settleOversizedStatus` runs before the
degrade: a 410 reclaims and a transient 5xx replays, and a body this size
is by construction one no revocation receipt can be read out of — which
`touchpointWithdrawalReclaims` already rules is the whole deployment being
withdrawn. Once the cache has answered, the remainder has no reader, so it
is dropped rather than buffered back over the ceiling.

* test(daemon): pin that a 410 reclaims when its body expands past the decode ceiling

The streaming ceiling had a hole; this is the same rule one ceiling further
in. A body small on the wire but oversized once expanded leaves `decoded`
null, and the status block must still run — it precedes the unparseable-body
bail-out, and nothing stated that order.

Verified the guard can fail: moving the `!decoded` return above the status
block (one site) turns exactly this spec red.

* fix(daemon): make a damaged record a miss, and make a wound-back clock still pass

Two ways the persisted cache could break the fail-closed guarantee it states.

`parseAssembly` validated every scalar it reads but not `envelope`, which
`rebuild` dereferences with `Object.entries`. A record that is valid JSON of
the current version with `envelope: null` therefore threw — from inside the
proxy's upstream `error`/5xx handlers, where nothing catches it, so cache
corruption could take the daemon down in place of the documented cache miss.
Validating it here keeps "damaged record" and "miss" the same outcome.

The high-water mark survives a restart, but on its own it cannot MEASURE
time while the wall clock sits behind it: `max` pins `now` to the mark,
`elapsed` stops growing, and an activity keeps its authority for as long as
the clock stays wound back. The defect is not that the mark is too high — it
is that the mark does not move. Anchoring it to this process's monotonic
reading makes the wait count, per key so one placement's mark is never
evidence about another's.

This keeps `does not let a backwards system clock extend the window` green
rather than reversing it: the ruling that observed time cannot be
un-observed stands, and is now paired with time that continues to be
observed.

Both specs were verified to fail without their fix: removing the `envelope`
guard throws `TypeError: Cannot convert undefined or null to object`, and
removing the anchor replays an activity 25 hours past its `endsAt`. The
other 16 specs stayed green through each ablation.

* test(daemon): pin that the clock residual is bounded by downtime, not by the rollback

The monotonic anchor cannot recover time the daemon was not running, so a
rollback straddling downtime still buys display time. How MUCH is the whole
question, and it was being argued rather than measured.

This fixture measures it: one hour of downtime plus a twenty-four hour
rollback costs exactly one hour of over-display — the activity ends after
twenty-four hours of UPTIME, not twenty-four hours past the rollback.

Verified it can fail: removing the anchor lets the same activity survive the
final assertion.

* fix(daemon): refuse offline replay after an uncertain clock restart

Do not restore cached display authority when startup local time is behind the persisted observation. Fresh server responses establish a new per-record monotonic anchor, preserving consistently offset clocks and in-process rollback handling.

---------

Co-authored-by: lefarcen <935902669@qq.com>
Co-authored-by: lefarcen <ontf116@gmail.com>
2026-09-23 09:22:48 +00:00
CheemsandGitHub 0450945f2d fix(daemon,web): keep a cleanly exited run succeeded when its task is blocked (#8350)
* fix(daemon): keep a cleanly exited run succeeded when its task is blocked

A Run records how its process ended; the strategy task records its own
verdict. The run settlement used to rewrite a clean exit to `failed`,
send an `OD_NEXT_TASK_BLOCKED` error frame and mark the message failed
whenever the task ended `blocked` at the production stage. That put a
red failure card and a "Run failed" execution record under a turn whose
files are in the project and whose preview renders — a turn that edited
a page other than the entry, or wrote the entry and answered in prose
without the machine block — because the card for that error code is the
same one a crashed process gets.

Remove the rewrite. A Run whose process exits 0 stays `succeeded` at
every stage; the terminal event still carries the task projection with
`outcome: blocked` and its reason codes, and the client decides from
those and the deliverable on disk what the turn produced: a project with
a usable entry keeps "Done", a turn that produced nothing still gets a
failure card drawn by the client from the reason code. A non-zero exit,
a signal, a timeout and an unavailable upstream fail the Run as before.

Tests: the production-block server case now asserts the Run ends
`succeeded` with exit code 0, no error frame, `strategyTask.outcome:
blocked` at `inputStage: production`, the message `runStatus` succeeded
and `run_finished` reporting `result: success` with the primary reason
code. The refused-planning-turn case is unchanged, and the
`OD_NEXT_TASK_BLOCKED` classifier case keeps covering Runs recorded by
earlier versions.

* fix(web): keep a succeeded run Done beside a blocked task

The shared rule `canRetainSuccessfulRunForBlockedStrategy` decided
whether a Run that succeeded kept its success on screen when the
strategy task ended `blocked`: only a Run that wrote the entry, a
project that already held one beside a reply, a refusal before
production with a reply, or a block the agent declared and explained
did; everything else was marked failed on the client and given a card
keyed on the reason code. With the daemon no longer failing such Runs,
that client-side failure was the one thing still turning a clean exit
into a failed turn — and on the live stream it left a generic card under
a turn whose folded footer read Done, while a reload rebuilt the same
turn as failed with the reason-specific card.

Make the rule the Run's own result: a succeeded Run keeps its success at
every stage, whatever the task recorded and whatever the agent said. The
verdict — outcome, reason codes, visible text — still reaches the message
through the settled task projection for the diagnostics; a failure card
is drawn only for a Run that did not succeed. The signature and the three
call sites (live settlement, cold history load, artifact recovery) are
unchanged, so each still hands over the facts it resolves.

Tests: the cases that expected a run error or a restored failure for a
succeeded Run beside a blocked task now expect Done, no error event and
the verdict on the settled projection (`sse.test.ts`,
`strategy-blocked-verdict.test.ts`,
`ProjectView.blocked-history-hydration.test.tsx`,
`ProjectView.artifact-recovery-verdict.test.tsx`). The artifact-recovery
suites that used a blocked task as the way into the failed-turn recovery
path now end their Run with a non-zero exit, so they keep guarding that
path; every case that asserts a card for a failed Run is unchanged.
2026-09-22 09:04:16 +00:00
Mad DinhandGitHub 0fbdeafd16 fix(web): restore theme tokens on the full-screen present-exit button (#7272)
The full-screen present-exit button was moved onto theme tokens in #4593,
but the rule was reverted to its hardcoded form while
apps/web/src/styles/viewer/composio.css was rewritten during a later
merge, so dark mode shows a bright white block over presented content
again.

Restore the merged treatment (--bg-elevated / --bg-muted, --border-strong,
--text, and the :focus-visible outline) and add a style test so the rule
cannot silently lose its tokens a second time.
2026-09-22 08:30:07 +00:00
CheemsandGitHub 753a23dc04 fix(daemon,web): end a refused planning turn as the agent's reply instead of a failed run (#8322)
* fix(daemon): end a refused planning turn as the agent's reply instead of a failed Run

When the strategy gate refuses a turn before production — a greeting,
an off-topic question, a request the agent declined to plan a build for
— the agent has already answered in prose and the process exited
cleanly. The settlement still rewrote that Run to failed, sent an
OD_NEXT_TASK_BLOCKED error frame and marked it non-retryable, so the
user saw a red "task could not complete" card under a reply that was
the whole outcome of the turn, and a refresh drew the card again from
the persisted failure.

The rewrite now applies only to a task whose input stage is production:
there the user asked for a deliverable, the plan was frozen, and nothing
usable was written, which is the failure it exists to report. A task
refused at the request, clarification or contract-repair stage keeps the
Run's own clean exit and sends no error frame. The task record still
carries the blocked verdict and its reason codes, the message's run
status follows the Run, and run_finished reports success with the
blocked reason code as before.

The regression test drives a planning turn that answers in prose with
no machine block through the real server and asserts the Run, the end
frame, the message row and the analytics record. The production-stage
regression keeps asserting the failed Run and the error frame.

* fix(web): keep a refused planning turn's reply on screen instead of a run error card

A blocked strategy task whose physical Run succeeded kept its success
in only two cases: the Run (or the project) held a usable deliverable,
or the agent had declared the block itself and explained it. Every
other block — a planning turn refused for a missing machine block — was
remapped to a failed turn with a "task could not complete" card, even
though the daemon now settles such a Run as succeeded and the agent's
reply is the outcome the user was waiting for.

canRetainSuccessfulRunForBlockedStrategy gains a third case: the task
was refused before it reached production and this turn has a visible
reply. The streaming settlement in the daemon provider now calls that
shared rule instead of an inline copy, so the live stream, the cold
history load and the artifact recovery decide the same way. A
production block with prose beside it still raises the card, and so
does a refused turn that left the user nothing to read.

Tests: the SSE case for a refused planning turn flips to "no error,
status succeeded" and gains a production counter-example; the cold
history hydration gains a request-stage Done case and a no-reply
failure case. Fixtures that describe a production gate refusing a turn
that put its output in the chat now say so with inputStage production,
so their existing assertions keep guarding that path.
2026-09-21 11:02:27 +00:00
0ffafb3d76 fix(chat): keep live strategy continuations in their original task (#8195)
* fix(chat): keep live strategy continuations in their original task

* fix(chat): re-derive the task position when a reattach follows a successor Run

`reattachDaemonRun`'s `onRunCreated` re-points the row at the daemon-created
successor Run but spread the predecessor message, so a page reloaded mid-task
kept the predecessor's `strategyTaskRunIndex` on a row that is now streaming a
different Run. That position is what `foldStrategyTaskTurns`, the fork boundary
and the successor-absorption rule read.

Resolve it through the same `strategyTaskRunIndex(...)` helper the live send
path uses, so a projection that does not name this Run exactly once yields
`undefined` instead of the predecessor's index.

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-09-20 08:55:21 +00:00
Huy PhamandGitHub 1aa228115f fix(web,daemon): preview snapshot bridge captured every real page as blank (#7125)
Two stacked bugs in the foreignObject snapshot bridge (the srcDoc bridge
in apps/web/src/runtime/srcdoc.ts and the URL-preview bridge the daemon
injects in apps/daemon/src/routes/project/index.ts share this code):

1. inlineSnapshotStyles stripped clone <script>/<link> nodes BEFORE
   pruneHiddenSnapshotNodes ran, but prune pairs the original/clone
   querySelectorAll('*') lists by index. The early removals shifted every
   later clone under the wrong original, so the misdirected hidden-node
   removals deleted visible content (often the <body> itself). Any page
   containing a script or stylesheet link rasterized as a uniform frame
   and the capture failed with 'empty-render' ("Preview is still
   loading. Try again in a moment."). The stripping now runs after prune
   (stripSnapshotResources).

2. bodyContent was serialized with innerHTML, whose HTML serialization
   emits void elements (<br>, <img>) without self-closing slashes -
   invalid XML inside <foreignObject>, so once bug 1 is fixed the SVG
   image fails to parse ('snapshot image failed'). Now serialized via
   XMLSerializer (serializeSnapshotXhtml), and XML-invalid attribute
   names (@click, :href) are dropped from the clone.

Desktop is unaffected because the host compositor path short-circuits
the bridge; every pure-web deployment (browser against the daemon, e.g.
the Docker/K8s setup in #5444) always takes it and has been broken.

Fixes #5444
2026-09-20 08:44:31 +00:00