Commit Graph
3135 Commits
Author SHA1 Message Date
AmyandGitHub e01efc78f6 test(e2e): align UI P0 coverage with the current product (#6405)
* test(e2e): preserve useful UI P0 hardening

* test(e2e): remove retired Automations coverage

* test(e2e): retain Automations smoke coverage

Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
2026-08-07 08:21:48 +00:00
lefarcenandGitHub f4b242d66c fix(collab): bound background shared-project pulls by manifest entry count (#6558)
* fix(collab): bound background shared-project pulls by manifest entry count

Publishing a shared project version made every member daemon materialize the
whole tree — hub push lane and catch-up sweeps alike — with no upper bound. In
incident #6512 that meant a 7442-file website clone landing on every workspace
member's disk, unasked.

An authorize-only Vela probe now reads the version's manifest entry count
BEFORE any blob transfer, and background lanes defer versions over the
threshold (default 2000, OD_COLLAB_BACKGROUND_PULL_MAX_ENTRIES) to the
foreground open-project pull, which never consults the guard. Deferrals are
remembered per exact scope+version so sweeps never re-authorize.

Fail-closed here means PULL AS BEFORE: an older bundled CLI without the probe,
a countless or mismatched receipt, and any probe failure all resolve to today's
behavior, so a project can never be left permanently unmaterialized.

* fix(collab): bind the size probe receipt to the full pull scope

The probe compared only projectId, version, and workspaceId, but a workspace
holds many principals: a receipt authorized for a different owner or viewer
was accepted as counted, and an oversized count there cached a deferral for a
scope that was never inspected — suppressing background materialization
instead of failing open as the guard promises.

Reuse the ordinary pull's receipt validator rather than re-listing fields, so
the probe cannot drift from the pull as the receipt contract evolves. Any
mismatch now returns uncounted (pull as before).
2026-08-07 08:21:27 +00:00
4de25d3f1b fix(web): 让长演讲者备注在 presenter 视图里滚动而非裁切 (#6271) (#6314)
问题:.presenter 视图里 .notes 这个 aside 作为 body 的 grid item 默认
min-height: auto,不收缩;body 的隐式 grid row 也 auto,跟着内容撑。
当备注文字到几百词时,.notes 把 body row 撑过 viewport,而 body
{ overflow: hidden } 直接切掉底部,.notes-body { overflow: auto }
永远不触发 —— 用户看不到下面的备注,也没有滚动条 (#6271)。

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

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

Co-authored-by: Ray Xi <2667192167@qq.com>
2026-08-07 07:54:30 +00:00
lefarcenandGitHub 7251259334 perf(collab): bound vela push fan-out and recover failed publishes (#6564)
* perf(collab): bound vela push fan-out and recover failed publishes

Three guardrails for the team-collab publish pipeline:

- Merge IGNORED_PROJECT_DIR_NAMES into the vela resource push exclude
  list (MEMBER_MIRROR_PUSH_EXCLUDED_ENTRIES). Generated/installed trees
  the project UI already hides from the owner (dist, .next, .venv,
  coverage, .tmp, __pycache__, ...) no longer fan out to member
  mirrors; every secret-bearing exclusion is preserved.
- Give every vela resource subcommand a wall-clock budget: push 10min
  (slow-uplink envelope that only reaps truly wedged children),
  head/shared/remove 60s, pull keeps its existing 30s. A hung CLI now
  surfaces as an ordinary publish failure instead of pinning the
  scheduler's in-flight publish forever.
- Retry failed publishes on a bounded 5s/15s/45s backoff (max 3
  consecutive retries). Success or a fresh author change resets the
  budget; a change landing mid-publish keeps the existing dirty
  last-write-wins path without stacking a retry; dispose() clears
  retry timers.

* fix(collab): scope generated-tree exclusions to directories

The owner side hides these names as DIRECTORIES only — `collectFiles`
consults `shouldSkipDir` inside its `isDirectory()` branch — but they were
handed to `vela resource push --exclude`, which matches an entry name
whatever its type. A project holding a regular file called `target`, `out`
or `dist` therefore had that file silently dropped from every member
mirror while the owner kept seeing it, so the two views disagreed with no
error anywhere. Found in QA on the previous commit.

Send those names in the directory-only trailing-slash form and keep the
secret-bearing entries bare, since a credential must stay behind whether
it is a file or a directory. The slash doubles as the compatibility seam:
a Vela build that does not understand it compares "dist/" against names
that never contain a slash, matches nothing, and publishes the full tree —
the pre-optimization payload, never a missing file. A new --exclude-dir
flag could not degrade that way, because older CLIs reject unknown flags
and every publish would fail instead.

`.git` and `node_modules` are dropped from the directory-only set: they
are already in the bare secret-bearing list, which covers every type.
2026-08-07 07:28:10 +00:00
lefarcenandGitHub cc7d8a5cd5 fix(web): recover deck preview after returning to a project tab (#6519)
* fix(web): recover deck preview after returning to a project tab

A workspace-scoped deck whose HTML references a relative project asset
holds its srcDoc preview empty until GET /api/projects/:id/files answers
(the #6142 scoped-asset rewrite hold). The viewer's file-list effect
called fetchProjectFiles without an AbortSignal, which pins the shared
single-flight entry: a stalled read (queued behind saturated
connections; every failure path resolves []) then survives unmount
forever and every later mount of the same project + workspace identity
rejoins the same dead promise — so leaving and returning to the project
showed a permanently white main stage while the thumbnail rail and
speaker notes (rendered from source directly) stayed healthy.

Two changes:
- Abort the file-list read on effect cleanup so a viewer's pending read
  dies with the viewer and a fresh mount always issues its own read.
- Cover the held (empty-srcdoc) preview with the standard
  artifact-preview-first-load loading indicator instead of showing a
  bare white iframe, mirroring the URL-load path's cover.

* fix(web): remount deck srcdoc preview after a hidden-tab load (#6587)

A deck that finishes parsing into its srcdoc iframe while the browser
tab is hidden never experiences a real layout pass: Chrome keeps the
hidden iframe's child viewport at 0x0, so the deck's one-shot fit
resolves to transform: scale(0), and the in-frame recovery loop
(chaseFirstLayout, 30 x 50ms resize nudges) is exhausted by
background-timer throttling long before the user returns. Neither the
host nor the srcdoc bridge listened for visibilitychange, so nothing
ever re-ran the fit — the main stage stayed permanently white while the
host-rendered thumbnail rail and speaker notes looked healthy.

The srcdoc onLoad handler now latches whether the load completed with
document.visibilityState === 'hidden'. On the first return to a visible
document, the active viewer remounts the srcdoc iframe for a fresh
parse (the same mechanism Comment re-activation already relies on) and
clears the latch, so visibility round-trips after a healthy visible
load never remount (no flicker) and retained viewers
(workspaceActive === false) are never touched. Slide position is
restored by the existing syncCachedSlideStateToIframe onLoad path.

* fix(web): stop pinning the srcDoc loading cover over a loaded empty file

The srcDoc first-load cover keyed on srcDoc emptiness alone
(!srcDocTransportContent), which cannot distinguish "content still
pending" (previewSource === null: the scoped-asset hold or a Reload's
synchronous clear) from "loaded, zero bytes" (previewSource === ''), so
a legitimate empty HTML file kept a permanent role=status/aria-busy
loader with no request in flight. Narrow the condition with an explicit
previewSource === null pending check; the redirect-loop-blocked and
lazy-transport branches are untouched.

* fix(web): run the hidden-load recovery check on viewer activation

The visibilitychange recovery listener only exists while the viewer is
active. When a retained deck's load completes in a hidden browser tab
(latch armed, correctly no action) and the browser returns to visible
BEFORE the user clicks back into the project, the event fires with no
listener armed and never replays — activation then re-arms the listener
but the latch dangles and the 0x0-parsed deck stays white on exactly
the switch-back-to-the-project-tab flow. Run one immediate check after
addEventListener: the handler's own guards (visible + latch + srcdoc
active + deck) make it a one-shot no-op everywhere else, and the latch
keeps it idempotent across effect re-runs.
2026-08-07 07:18:09 +00:00
Chanhyo JungandGitHub 254d312876 fix(daemon): allow workspace-less design system deletion (#6591) 2026-08-07 07:06:21 +00:00
Albert UnchiasuandGitHub 5e826dc1e2 fix(daemon): read Qwen Code's configured models instead of two hardcoded ids (#6546)
The Qwen picker was pinned to a static two-entry fallback
(qwen3-coder-plus / qwen3-coder-flash) because qwenAgentDef declared
fallbackModels with no fetchModels. Qwen Code has no --list-models flag,
but it does persist its configured models in ~/.qwen/settings.json under
modelProviders.<provider>[].id plus the selected 'model'. On a current
install that file lists qwen3.8-max, qwen3.7-max, qwen3.7-plus,
qwen3.6-plus and others, none of which the picker could offer — the user
had to retype one as a custom model string.

Add runtimes/qwen-settings.ts, mirroring mmd-routes.ts (the same problem
solved for Claude Code): read the CLI's own config, merge discovered ids
ahead of the static fallbacks, and degrade to the fallbacks when the file
is missing or malformed. Provider keys are not allowlisted so a Qwen
release that adds one keeps working, and only model ids are read — the
DASHSCOPE_API_KEY that lives in the same file is never touched.

5 new tests in tests/runtimes/qwen-settings.test.ts, including explicit
assertions that no secret from settings.json reaches the model list.
tests/runtimes: 694 passed, 1 skipped. tsc --noEmit clean for src+tests.
2026-08-07 07:05:54 +00:00
lefarcenandGitHub 34192c6719 fix(diagnostics): keep the previous session's daemon log and cut event tails on line boundaries (#6531)
* fix(diagnostics): keep pre-restart daemon log and line-aligned event tails

Two gaps made incident support bundles useless after a relaunch:

1. Every packaged launch opened each sidecar's latest.log with mode
   "w", destroying the previous session's log — the bundle held only
   the ~70 lines written since the restart while the incident-time
   daemon log was gone. openLog now rotates the prior file aside as
   previous.log (exactly one prior session, rename overwrites), and
   the diagnostics export bundles logs/<app>/previous.log whenever it
   exists, under the same per-file tail cap. The source entry is
   existence-conditional so non-rotating launchers (tools-dev appends)
   do not gain missing-file placeholder noise.

2. The 2MB tail cap on runs/*/events.jsonl cut bytes from mid-file, so
   the first kept line was half a JSON object and jsonl consumers fell
   over on line 1. readMaybeTail now advances the cut to the next line
   boundary (with a guard byte so an already-aligned cut loses
   nothing); a window with no newline keeps the raw cut.

* fix(packaged): keep the prior log when rotation fails instead of truncating it

A non-ENOENT rename failure (Windows share-lock on previous.log, read-only or
exotic filesystem) still fell through to open(path, "w"), destroying the only
copy of the pre-restart log while previous.log stayed unavailable — the exact
data loss this rotation exists to prevent. Truncate only when the prior bytes
are provably elsewhere: moved aside, or never written (first-launch ENOENT).
Otherwise open in append mode, since a merged two-session log is recoverable
and an erased one is not.

* fix(diagnostics): never export a partial record, and never hide an unreadable log

Two holes in the truncation/collection contract:

- A single event larger than the whole tail budget left no complete line to
  align to, and the fallback kept the raw cut — so the export shipped the
  interior of one JSON record and any consumer streaming the bundle through a
  jsonl parser died on line 1, exactly what alignment exists to prevent. Signal
  "no complete line" instead and let the collector record it, so the bundle
  carries a self-describing placeholder rather than half an object.
- The optional-source gate treated EVERY access failure as absence, so a
  previous.log that exists but is unreachable (EACCES on the log directory,
  EIO) vanished with no manifest entry and no warning. Only ENOENT may drop the
  entry now; anything else stays listed and reports the real error.
2026-08-07 07:04:52 +00:00
0d00eb298a feat(daemon): forward skills[] as skillIds[] on start_run MCP tool (#6429)
The MCP bridge exposed only a single skill on start_run; any extra
skills[] an agent passed were silently stripped by additionalProperties:
false and the handler reading only args.skill. The daemon already
composes supplemental skill ids server-side (POST /api/runs spreads the
body; composeDaemonSystemPrompt dedupes and stages each skill), so the
bridge just needs to serialize them.

- McpArgs: add skills?: string[]
- start_run inputSchema: add skills (array of strings)
- startRun(): forward args.skills as body.skillIds

Tests: start_run POSTs skills[] as skillIds; skillIds omitted when
skills not provided.

Co-authored-by: Newton <newton@mcclean.codes>
2026-08-07 12:27:34 +08:00
lefarcenandGitHub 1347ce4e48 fix(packaged): fail od:// proxy fast on local resource exhaustion (#6530)
When the machine is out of sockets/file descriptors
(net::ERR_INSUFFICIENT_RESOURCES, EMFILE, ENFILE, EADDRNOTAVAIL,
ENOBUFS), the proxy's two resilience layers amplified the outage:
the undici fallback re-issued every failed net.fetch (7890 doubled
requests measured in the incident) and the linear-backoff transient
retry stacked another 1334 attempts on top — each consuming exactly
the resource the machine had run out of.

Classify these errors through one whitelist helper
(isLocalResourceExhaustionError, matching both Error.code and the
message since Electron net.fetch reports net::ERR_... in the message
with no code) and make both layers fail fast: no undici fallback, no
retry. Everything outside the whitelist keeps today's behavior —
ECONNREFUSED still buys the full retry budget (daemon startup race)
and generic net.fetch rejections still fall back to undici (CSS-mask
GET rescue).
2026-08-07 12:10:47 +08:00
3fce756b5e docs(landing): publish Open Design 0.17 and 0.18 release notes (#6585)
Co-authored-by: xne998808-ai <276262049+xne998808-ai@users.noreply.github.com>
2026-08-07 11:59:08 +08:00
lefarcenandGitHub 19d66e9294 docs(release): add 0.18.1 release notes (#6560)
* docs(release): add 0.18.1 release notes

* docs(release): credit the desktop sign-in hand-off in 0.18.1 notes

* docs(release): correct campaign window, cloud fix scope, and BYOK claim

* docs(release): correct cloud fix scope and BYOK claim in 0.18.1 notes
2026-08-07 11:24:28 +08:00
lefarcenandGitHub 23c5f421bf ci(backport): label conflicted backports so the release pre-flight check has a signal (#6574) 2026-08-07 11:22:48 +08:00
lefarcenandGitHub 9065a67a5b fix(web): stop provisional comment pin numbers colliding after deletion (#6517)
* fix(web): stop provisional comment pin numbers colliding after deletion

The provisional pin bubble for a brand-new, not-yet-saved comment was
numbered comments.length + 1, but the daemon assigns MAX(pin_seq) + 1 on
create. Pin numbers are permanent (a deletion retires its number), so
after deleting an earlier comment the count-based guess collided with a
pin still on the canvas (e.g. one surviving comment numbered 2 -> new
pin also showed 2).

Introduce provisionalNextPinNumber() in apps/web/src/comments.ts, which
mirrors the daemon's MAX(pin_seq)+1 invariant over the rendered marker
numbers, and use it for the active-pin preview in FileViewer.

* fix(web): retire non-open comments' pin numbers in the provisional pin

Review follow-up (PR #6517): the provisional pin number was computed over
creationSortedSideComments, which filters to the current file AND
status === 'open' — narrower than the daemon's MAX(pin_seq) scope, which
scans every row for the project/file. A resolved/attached/failed comment
keeps its pin_seq row, so with a hidden pinSeq of 2 and no open pins the
bubble showed 1 while the daemon would assign 3.

Compute provisionalNextPinNumber over the file's comments across ALL
statuses in FileViewer and pass the result into CommentPreviewOverlays,
so the overlay's open-only marker list stays untouched while the
provisional number mirrors the daemon's full scope.
2026-08-07 10:58:58 +08:00
Marc ChanandGitHub a1995a186a test(e2e): harden UI flake waits with real completion signals (#6539)
* test(e2e): harden UI flake waits with real completion signals

Replace synthetic /projects routing, blind boot sleeps, and call-site
create retries with observable readiness: real navigation, suite daemon
health warmup, expect.poll, clock fast-forward for coalesce TTL, and
quiescence for AMR refresh traffic.

* test(e2e): raise Playwright fixture timeout for daemon warmup

The suite fixture now polls daemon /api/health for up to 60s, which
consumed the prior 390s budget's bookkeeping margin. Raise the worker
fixture timeout to 450s so start + web/daemon warmup + stop still leave
a full minute for teardown bookkeeping on slow CI workers.

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

* test(e2e): fail closed on health body drain and narrow billing mock

Require warmPlaywrightDaemonRuntime to fully consume ok health bodies
before declaring readiness, drain non-ok probes, and fulfill only the
exact /api/workspace/billing path so nested billing routes fall through.

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

* test(e2e): reopen projects by id in cross-project file-tab restore

Avoid relying on chrome workspace tabs surviving leave+create hard
navigation; assert per-project file-tab restore via project routes.
2026-08-07 10:37:59 +08:00
lefarcenandGitHub caae6a373e feat(web): localize DeepSeek V4 Flash campaign (19 locales) (#6561)
* feat(web): localize DeepSeek V4 Flash campaign across 19 locales

Move all campaign user-facing copy into Dict keys so modal, model badges,
and the workbench pricing badge follow the active UI language. Also route
personal free accounts through amrAccountPlan and portal the campaign badge
above workspace-tabs chrome so it stays clickable.

* fix(web): fully translate DeepSeek campaign copy for all 19 locales

Replace English placeholders in incomplete locale packs and add a matrix
test so non-English campaign strings cannot silently equal the EN fallback
(with an explicit allowlist for intentional day-unit reuse).

* ci: re-trigger validation after runner queue stall

* ci: bootstrap Resolve runner profiles on Blacksmith when mode is blacksmith

OD_CI_RUNNER_MODE=blacksmith still scheduled the profile-resolution job on
nexu-runners-small, so a private ARC stall blocked every trusted PR before
workloads could reach Blacksmith. Align bootstrap with the mode contract.

* ci: ensure pull_request workflow fires for blacksmith bootstrap

* revert: drop Blacksmith CI bootstrap change from campaign PR

Keep this PR scoped to DeepSeek campaign i18n + badge/audience fixes.
The runner-bootstrap alignment belongs in a separate CI PR with topology
coverage, not a backport-labeled product fix.

* ci: re-run validation on GitHub-hosted runners (economic mode)

* ci: re-fire pull_request checks for Validate workspace
2026-08-07 07:48:50 +08:00
Marc ChanandGitHub 4e47119edb ci: right-size UI P0 runners to medium (#6502)
Measured UI P0 shards fit nexu-runners-medium; drop the blanket xlarge
assignment and keep only project-collab on ui_p0_heavy (xlarge) because
multi-client collab has hit large/xlarge memory ceilings.
2026-08-06 21:17:25 +08:00
AmyandGitHub 5580736c1a fix(e2e): align packaged onboarding smoke selector (#6538)
* fix(e2e): align packaged onboarding smoke selector

* test(e2e): guard packaged onboarding selector drift

* fix(e2e): align platform onboarding probes

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

* fix(e2e): align packaged smoke with identity gate

Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
2026-08-06 20:00:13 +08:00
8b3ec59cfe feat(campaign): DeepSeek V4 Flash free-week surfaces (landing + web + attribution) (#6507)
* feat(campaign): port DeepSeek V4 Flash free-week surfaces from #6450

1:1 port of the demo net diff (base feat/workspace-team) onto main.
Landing home banner, landing pricing benefits, cloud-team-demo review
page, in-app home modal, workbench badge, model-switcher benefit badge,
and the campaign attribution/analytics pipeline. No behavior changes in
this commit; agreed behavior fixes land separately.

* fix(campaign): show the DeepSeek V4 Flash modal only on the active home view

The dialog portals to document.body while EntryShell hides inactive entry
views with display:none, so the campaign escaped the home view and
interrupted projects/tasks/plugins routes. Thread EntryShell's existing
isActive signal through HomeView into a new `active` prop: when inactive
the modal neither opens nor renders (and closes without being marked seen,
so returning to home while still unseen re-opens it).

Requirement: 弹窗只在 #/home 展示,不在编辑器或其他工作流中打断用户。

* fix(campaign): fail closed when frequency-control storage is unreadable

hasSeenCampaign() answered "unseen" when localStorage threw (private
mode / storage disabled), so the modal re-opened on every Home mount even
though markCampaignSeen() could never persist the dismissal. The campaign
promise is one appearance per activity window — an unreadable store now
counts as seen.

* feat(campaign): make the paid 立即使用 CTA switch to DeepSeek V4 Flash for real

产品拍板 D5 + Demo 表格「点击立即使用模型切换为DSV4」: the paid modal CTA
previously only clicked the composer chip open and highlighted it. Now it
performs the actual switch — agent to Cloud (amr), model to
deepseek-v4-flash — through EntryShell's onAgentChange/onAgentModelChange
persistence pair (the same sink InlineModelSwitcher writes through),
threaded HomeView → DeepSeekV4FlashCampaign as onUseCampaignModel. The
chip keeps the data-campaign-highlight pulse as feedback but no longer
opens the picker; the use_now analytics element is unchanged.

* fix(campaign): carry metricsConsent + device id on the unpaid modal upgrade

The unpaid modal's recordAmrEntry was the only campaign touchpoint that
dropped metricsConsent — the workbench badge and the model-switcher
upgrade already forward it — so the consent-gated AMR analytics mirror
never fired and the plans URL missed od_device_id. Thread
config.telemetry.metrics and config.installationId from EntryShell through
HomeView into the modal, and stamp the URL via
amrHandoffDeviceId + attributedAmrUrl(plansUrl, attribution, deviceId).

* fix(campaign): make the campaign/campaignAudience/campaignUsage review params production-inert

产品拍板 D7 + 需求文档「该入口仅用于本地 Demo,不作为线上产品规则」: the
review URL fixtures forced campaign surfaces for real users on released
builds.

Web: new isCampaignReviewAllowed() gate (NODE_ENV !== 'production',
inlined at build time) now fronts isDeepSeekV4FlashCampaignReview, the
campaignAudience override, the modal's shouldForceCampaignReview, and the
switcher's campaignUsage read.

Landing (Astro): inline scripts cannot see build-time constants, so the
home banner, pricing page, and cloud-team-demo inject the verdict as
data-campaign-review-allowed (__OD_LANDING_NOINDEX__: staging/PR previews
true, production false) and preview activation now requires it. The
[locale] wrappers re-render the same components, so they inherit the gate.

* fix(landing): stamp campaign attribution only inside the activity window

Pricing subscribe CTAs and the cloud-team-demo upgrade/pricing clicks
minted __odRecordCampaignEntry with campaignId and stamped
campaign_id on analytics props unconditionally, so clicks outside the
fixed Aug 6-13 window were counted toward the campaign. Both surfaces now
pass the campaign id and the campaign_id prop only while campaignActive
is true; od_entry_* attribution itself is still recorded outside the
window, just without the campaign stamp. Inbound first-touch
od_campaign_id preservation in posthog-analytics.ts is untouched.

* test(campaign): use undefined-safe access for window.open mock calls

Strict TS (tsc -b) flags open.mock.calls[0][0] as possibly undefined in
the modal spec added for the unpaid-upgrade consent behavior; go through
String(...[0]?.[0]) instead. No behavioral change.

* chore(campaign): drop demo-only artifacts before shipping

Remove the cloud-team-demo static review page (fake team cards and
prices, noindex but publicly reachable — review-only material that must
not ship), its locale wrapper and test, and five unreferenced campaign
poster PNGs left over from demo iterations. The team-modal campaign
surfaces the page previewed live in the real Cloud plan-selection
component (powerformer/vela#1379).

* fix(campaign): make review URLs a pure UI projection with zero config writes

Product boundary ruling: the client-side campaign change is a badge next
to DeepSeek V4 Flash (auto-hides after the window) plus the modal CTA
reusing the existing model-selection callbacks — selection behavior must
not change. The demo's review fixtures violated that: an effect force-
switched the persisted agent to AMR under ?campaignAudience=, and the
model write-back effect persisted the review projection into the user's
config. Both writes are removed; review URLs now project AMR + DeepSeek
purely in the UI and leave persisted state untouched.

This also roots out the CI heap OOM in
InlineModelSwitcher.compact-model-click.test.tsx (run 31076791463):
the stateful test wrapper re-rendered on every persisted write, looping
the unconditional write-back forever. With review write-back gone the
file completes in ~10s. The review-URL test now asserts the projection
and that onAgentChange/onAgentModelChange are never called.

* refactor(campaign): remove DeepSeek V4 Flash URL review backdoors

Product decision: the ?campaign= / ?campaignAudience= / ?campaignUsage=
review parameter mechanisms are deleted entirely. Campaign visibility is
now decided only by the real activity window plus the real subscription
audience, and pre-launch acceptance switches to a temporary startAt
constant override instead of URL fixtures.

web:
- campaigns/deepseek-v4-flash.ts: drop review param constants, review
  gate, review detection, and the audience URL override; visibility and
  audience resolution take only `now` (plus plan/loggedIn); restricted
  presentation kept as a reserved branch for the backend usage-limit
  signal (no trigger wired yet)
- DeepSeekV4FlashCampaign.tsx: drop the forced review open and the
  synthetic openedAt+7d countdown; the countdown always runs against the
  real endAtExclusive boundary
- InlineModelSwitcher.tsx: drop campaignUsage URL read, audience
  override, review agent/model fixtures and the currentAgent/model
  review projection (incl. data-campaign-review); 已暂停 and 升级可用
  badge branches stay as reserved presentation flags with no trigger
- EntryShell.tsx / use-deepseek-v4-flash-campaign.ts: no more
  location.search plumbing; home-hero.css review-width rule removed

landing-page:
- index.astro / pricing/index.astro: preview branch removed (review
  params, review-allowed attribute, synthetic preview countdown);
  campaignActive follows the fixed window only
- _lib/deepseek-v4-flash-campaign.ts: reviewParam field removed

tests: review-only specs deleted; convertible specs rewritten as pure
time-window assertions (clock pinned inside/outside the real window);
new negative specs pin that no campaign surface reads URL parameters.

* fix(campaign): device id on badge handoff + impressions on window-open transition

Two review findings from PerishCode, both verified real:

- EntryShell workbench-badge handoff called attributedAmrUrl without the
  device id, so opted-in clicks omitted od_device_id (confirmed in live
  browser acceptance: the modal URL carried it, the badge URL did not).
  Now derives it via amrHandoffDeviceId like the other two touchpoints;
  the source-contract assertion pins the three-arg call.
- The pricing page emitted campaign impressions only during initial
  script execution, so a tab opened before startAt showed the surfaces
  at launch without any surface_view. Impressions now share the reveal
  loop's cadence with a one-time flag, covering both load-time and
  mid-session window-open transitions.

* temp!: open the campaign window early for pre-launch acceptance — REVERT BEFORE MERGE

startAt 2026-08-06T20:00 → 00:00 (+08:00) in both front-end constants so
reviewers can see the real campaign state (true countdown, no review
backdoors) on local runs and the PR preview today. endAtExclusive is
untouched. This commit MUST be reverted before this PR enters the merge
queue — merging it would launch the landing surfaces ahead of 20:00.

* test(campaign): derive the start-instant countdown from the window constants

Pins the assertion to endAtExclusive - startAt instead of a literal
'7天 00:00:00', so the temporary pre-launch start-time override (and any
future window change) cannot silently break the suite.

* Revert "temp!: open the campaign window early for pre-launch acceptance — REVERT BEFORE MERGE"

This reverts commit f123289c0f.

* Reapply "temp!: open the campaign window early for pre-launch acceptance — REVERT BEFORE MERGE"

This reverts commit e472cc81d2.

* test(e2e): seed campaign-modal dismissal into every Playwright context

The DeepSeek V4 Flash home modal fires once per fresh browser profile the
moment the campaign window is open — which is every CI run from launch
onward. It steals focus and covers the elements functional specs are
about to drive, surfacing as 14-minute waitForResponse timeouts in the
UI P0 groups. Seed the one-shot dismissal (a legitimate user state the
product already persists) through a shared context fixture on both test
and clusterTest, so specs stay deterministic inside and outside the
window. One storage entry per campaign; keys stay in sync with the
component's SEEN_KEY.

* test(e2e): seed campaign dismissals into collab cluster contexts

createCollabCluster allocates its own BrowserContext via browser.newContext(),
so the suite-level context fixture never runs for multi-client specs. That left
the DeepSeek V4 Flash home modal free to open and block ensureRailOpen clicks
(UI P0 project-collab / project-workspace-editor). Share the dismissal seeder
and apply it before each cluster client's first page.

* test(e2e): move project collab scenarios to P1

* test(ci): remove project collab from P0 matrix

* test(e2e): mock campaign init scripts in collab cluster

* fix(e2e): restore project-collab P0 gate and collab mock seeding

PerishCode correctly blocked the follow-up that retagged the multi-client
convergence specs as P1 and dropped project-collab from the merge-gated UI P0
matrix: those two cases are the only browser coverage for live two-client
content/presence/unshare and authority-revocation convergence.

- Restore [P0] tags, the project-collab group/matrix/coverage entries, and the
  six-domain / four-domain shadow contracts (matches main).
- Teach collab-cluster unit mocks about addInitScript so the campaign-
  dismissal seed on independently created BrowserContexts does not break E2E
  Vitest.

* fix(campaign): close paid-CTA mode switch and attribution gaps

Address Siri-Ray's blocking review on head 61de050:

1. Paid 立即使用 now flips execution mode to daemon before writing amr +
   deepseek-v4-flash, so a BYOK (mode=api) user does not keep the BYOK
   provider after the modal CTA.
2. Landing campaign helpers preserve od_device_id across Pricing → Cloud,
   treat campaign membership as explicit (no stale inbound od_campaign_id
   inheritance), and Pricing only re-stamps campaign id while active.
3. Daemon AmrEntryAnalyticsPayload/parser validate and forward campaignId +
   conversionSource so the consented AMR mirror no longer strips them.

---------

Co-authored-by: mrcfps <mrc@powerformer.com>
Co-authored-by: Amy <1184569493@qq.com>
2026-08-06 19:51:33 +08:00
Ray XiandGitHub 370479e4a7 fix daemon Langfuse tool status noise (#6457) 2026-08-06 19:06:21 +08:00
3fb17262bb test(daemon): stabilize environment-sensitive baselines (#6371)
Co-authored-by: Ray Xi <2667192167@qq.com>
2026-08-06 19:05:47 +08:00
b18aad6bbe Add Shanghai workshop recap (#6440)
Co-authored-by: koki yanlai xu <koki@kokideMacBook-Air.local>
2026-08-06 18:51:11 +08:00
lefarcenandGitHub 1aad2f5ef6 fix(release): treat an empty windows smoke mode as core (#6524)
* test(e2e): pin that an empty windows smoke mode means core

Two independent holes let an empty value read as a different profile:

The workflow-level one — a workflow_call `default:` applies only when an
input is OMITTED, so forwarding '' defeats the declared `core` default.

The spec-level one — `??` falls back only on null/undefined, so an empty
environment variable survives it and `smokeProfile === 'core'` is false.

Together they made release/v0.18.1's first prerelease take the `full` path,
demand the updater fixture only a genuine `full` request configures, and die
before the smoke started — on the branch-cut commit, before anything else had
landed there. release/v0.18.0 never showed it because its branch name matched
a special case that produced `skip`, so its smoke never ran at all.

Red on both layers: the resolver mirrors main's `?? 'core'` verbatim.

* fix(release): treat an empty windows smoke mode as core

Close both layers that let an empty value read as a different profile.

notify-release-feishu now names `core` as its final arm instead of ''. A
workflow_call `default:` applies only when an input is omitted, so forwarding
an empty string defeated the callee's declared default.

win.spec.ts resolves the profile through `resolvePackagedSmokeProfile`, which
treats empty, whitespace and unset alike as `core` and rejects anything it does
not recognise — a typo must not select the updater path the way '' did.

Either fix alone would unblock release/v0.18.1, but they guard different holes:
one stops an empty value being produced, the other stops one being misread.
2026-08-06 18:49:12 +08:00
lefarcenandGitHub b014b969c6 fix(web): lazy-render design file grids and gate thumbnail fetches (#6513)
* fix(web): lazy-render design file grids and gate thumbnail fetches

The design files panel root view intentionally lists every nested file,
so a web-clone project can put 4000+ HTML files in one section. Every
page card rendered at once, and every HtmlCardThumbnail fetched its
content on mount, exhausting local sockets
(net::ERR_INSUFFICIENT_RESOURCES) and starving the web<->daemon proxy.

- Thumbnail content fetches now start only once a card nears the
  viewport (IntersectionObserver, 800px bottom rootMargin) and pass
  through a FIFO pool capped at 6 concurrent fetches; slots return on
  settle and on unmount, including tasks still queued.
- The page-card grid and image masonry render incrementally (48 per
  batch) behind an invisible end-of-grid sentinel keyed per batch, so
  the grid reads exactly like a fully rendered one; directory and tab
  changes reset the revealed count.
- Environments without IntersectionObserver keep the previous eager
  behavior.

* fix(web): hold thumbnail fetch slots until the request settles

Review follow-up: releasing a slot during effect cleanup freed pool
capacity while the underlying fetch was still on the network, so a
directory/project switch with six thumbnails pending could start six
replacement fetches and push real connection concurrency to twelve.

Cleanup now only abandons the reservation: queued tasks are dropped
before they start, but a started request keeps its slot until it
settles, so the cap bounds actual network concurrency across
unmount/remount churn. The regression test encodes the exact
switch-while-pending scenario.
2026-08-06 18:48:51 +08:00
886bcc4db0 feat(web): streamline onboarding model source flow (#6475)
* feat(web): streamline onboarding model source flow

* test(e2e): align onboarding coverage with cloud-first entry

* test(e2e): isolate cloud status in non-auth specs

* test(e2e): keep restoration focused on tab state

* test(e2e): close cloud-first onboarding gaps

* test(e2e): align runtime checks with cloud-first gate

* ci(e2e): retire automations from P0 gate

---------

Co-authored-by: Amy <1184569493@qq.com>
2026-08-06 18:48:33 +08:00
99fc8a45bc Revert "feat(design-systems): add structured runtime contract (#6456)" (#6520)
This reverts commit 2b8aa8c457.

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
2026-08-06 18:48:11 +08:00
lefarcenandGitHub ad4f3609ca fix(collab): quell the packaged-client cloud 502 storm (heartbeat backoff, upstream error relay, proxy replay) (#6521)
* test(collab): red specs for the packaged-client presence/proxy 502 storm

Encode the three client-side halves of the 502 storm as failing specs on
main before any fix:

- web: a persistently failing presence upstream is heartbeated every 10s
  forever (62 POSTs in 10 minutes) with no backoff.
- daemon: every vela CLI presence failure is flattened into a 502 whose
  message embeds the spawned command line; upstream 403/404 rejections are
  indistinguishable from infra outages, every beat re-spawns the CLI, and
  presence spawns carry no timeout.
- web sidecar: an idempotent /api request that dies with a connection reset
  on a reused pooled daemon socket surfaces as a synthesized 502 instead of
  being replayed once on a fresh connection.

createDaemonProxyHandler is exported as the proxy spec's seam.

* fix(collab): stop the packaged-client presence/proxy 502 storm

Three client-side halves of one incident (hours of ~2050 DevTools errors:
POST /presence/heartbeat and PUT /workspace/billing/interests 502ing every
few seconds while the collab upstream rejected one project):

- web CollabClient: presence heartbeats move from a fixed 10s interval to a
  self-scheduling chain with exponential backoff on consecutive failures
  (10s -> 20s -> 40s -> ... capped at 5min, +/-10% jitter). One success or a
  new presence identity restores the base cadence, so recovery is untouched.

- daemon presence routes: vela CLI failures are classified via the CLI's
  'API request failed with status NNN' output. Upstream 401/403 relay as
  403, 404 as 404, timeouts as 503; only genuine infrastructure failures
  keep 502. Response bodies no longer embed the spawned command line or
  stderr (daemon log keeps the full detail). Repeated consecutive failures
  for one project open a 20s negative cache that answers without spawning
  another CLI process, and presence spawns now carry a 10s timeoutMs so a
  wedged CLI cannot pile up children.

- web sidecar proxy: proxied requests use an explicit keep-alive agent whose
  pooled idle sockets die well before the daemon's 120s keep-alive window,
  and an idempotent request with a small fully-buffered body that fails with
  ECONNRESET/EPIPE on a reused pooled socket before any response bytes is
  replayed once on a fresh connection instead of surfacing a synthesized 502
  the daemon never sent. Non-idempotent methods and large bodies keep the
  streaming path and are never replayed.
2026-08-06 18:47:07 +08:00
lefarcenandGitHub 9ce7b3aeec fix(byok): restore BYOK model switching in the home and composer pickers (#6515)
* fix(byok): restore BYOK model switching in the home and composer pickers

Two #6142 regressions with the same root shape: a model-picker surface
stopped deriving its catalogue from the active execution mode.

- InlineModelSwitcher: the compact home popover unconditionally rendered
  the local CLI agent's models, so a BYOK chip (e.g. gpt-4o) opened onto
  the Open Design cloud catalogue. The popover body now always reflects
  config.mode; compact only chooses layout density.
- AvatarMenu: the project composer popover collapsed the BYOK model area
  into a read-only box, dropping the previously working model switcher.
  BYOK mode now offers a selectable provider catalogue (shared cache +
  local discovery fetch + curated seeds) writing through onApiModelChange,
  mirroring the daemon-mode list.

* test(e2e): expect the BYOK catalogue in the topbar switcher visual case

The settings-workspace visual case pinned the #6142 regression as expected
behavior: it asserted the compact BYOK popover has no in-place model
dropdown and only a Settings hint. With the BYOK catalogue restored, the
case now asserts the provider model picker renders in place (current model
shown, options selectable from a deterministic mocked catalogue) while the
execution-configuration guards stay: no mode segmented control and no
provider tabs in the top bar, and the Settings route entry remains.
2026-08-06 18:45:56 +08:00
PerishFireandGitHub 2a971e4f0e ci: add Blacksmith runner mode (#6533) 2026-08-06 18:44:44 +08:00
Marc ChanandGitHub 66004e0cd7 test: raise CI flake budgets for waitFor and wall clocks (#6415)
* test: raise CI flake budgets for waitFor and wall clocks

Web jsdom waitFor was dying on the 1s default under runner contention.
Daemon deploy wall-clock asserts, logo body-timeout margin, and vela
fixed sleeps had zero/thin slack on slow CI CPUs.

* test: scope flake budgets and restore ReDoS guard

Keep the doctype prolog linear-time assert at 500ms so a reintroduced
lazy-body regex (~5s) cannot pass. Drop the global waitFor 5s raise and
apply a 2s budget only to the App.connectors privacy-choice waits that
flaked at 1015–1093ms under CI contention.

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

* test: restore CSS budget and abort-aware logo body mock

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

* test: wait for Vela login close before no-fallback assert

loginInFlight can go idle between child exit and close after cancel,
so the canceled late-fallback test now waits on supervisor settlement
(close/error terminal + pending fallback) instead of status idle.

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

* test: narrow AbortSignal for logo body mock typecheck

Preflight failed because init.signal was still possibly null after the
optional guard. Capture a local signal so tsc can prove it is defined.

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

* test: use global Testing Library asyncUtilTimeout 5s

Raise waitFor/findBy failure budget in the shared jsdom setup instead of
scoping a 2s override to App.connectors privacy cases. Drop no-op deploy
comment churn that did not change the ReDoS wall-clock guards.

* test: scope waitFor budget to privacy waits only

Drop suite-wide Testing Library asyncUtilTimeout 5s so unrelated
waitFor/findBy failures still surface assertion errors quickly.
Restore a 2s timeout only on App.connectors first-run privacy waits
that flaked under CI contention.

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

* Revert "test: scope waitFor budget to privacy waits only"

This reverts commit 7b41997565.

* test: set asyncUtilTimeout to 10s to avoid 5s timer spies

Product code and tests hardcode 5000ms (GitHub connector status,
brand poll). Spies that fire timeout === 5000 immediately also
hijacked Testing Library's waitFor overall timer when the global
budget was 5s. Keep the suite-wide failure budget; pick 10s.

* test: tighten async timeout and completion assertions
2026-08-06 07:57:25 +00:00
lefarcenandGitHub 72b1db761b fix(e2e): let the Windows packaged smoke settle on the cloud sign-in landing (#6481)
* test(e2e): pin the Windows packaged app-shell rule (red)

Extract the Windows packaged smoke's app-shell DOM probe and the rule that
reads it into a pure module, and cover both from a platform that can actually
run them. The probe reaches the packaged renderer as a plain string through
`tools-pack inspect --expr`, and `specs/win.spec.ts` only executes on a Windows
runner with a packaged build installed, so today neither is checked anywhere.

Behavior is unchanged: the rule still accepts only `homeVisible === true`, the
same test main's inline `ensureMainAppShell` applies. The two new cases that
describe a first run resting on the cloud sign-in landing therefore fail, which
is the bug — a packaged first run nobody signs in to can never reach home
unattended, so the wait can only time out.

    × settles on the cloud sign-in landing when the profile only needs a rendered surface
    × still requires home when the profile has to drive the entry rail

    AssertionError: expected null to be 'onboarding-landing' // Object.is equality
    - Expected: "onboarding-landing"
    + Received: null

    Test Files  1 failed (1)
         Tests  2 failed | 10 passed (12)

* fix(e2e): accept the packaged Windows onboarding landing as a settled shell

A packaged first run that nobody signs in to comes to rest on the cloud
sign-in landing, and `connectStepRuntimeReady` in EntryShell.tsx will not
advance without a signed-in cloud account, an installed local CLI, or a
verified BYOK key — none of which a release runner has. The core Windows smoke
only screenshots from this point, so the landing is a legitimate terminal
state there; demanding home made the wait time out on a healthy app.

The landing is recognised positively, from the same three affordances the [P0]
onboarding smoke asserts (sign-in CTA plus both runtime links), so a blank
window, a crashed renderer, a boot still on the loader, or an onboarding shell
that mounted and then died all still fail — with a named cause rather than an
opaque timeout.

The full profile goes on to click `entry-nav-updater`, which
`clickUpdaterRailExpression` refuses while onboarding is up, so it keeps
demanding home and now fails here with that stated reason instead of later
with a bare `onboarding-visible`.

    Test Files  1 passed (1)
         Tests  12 passed (12)

* test(e2e): pin the seeded-onboarding postcondition (red)

PerishCode's review on #6481: the core run seeds onboarding as completed and
then accepts onboarding anyway, so a broken completed-onboarding boot path
passes silently.

Name the decision — `packagedAppShellPolicy` — and cover it. The
implementation still keys off the smoke profile, exactly as the previous commit
left it, so the cases that say a run whose daemon confirms completion must
still require home fail.

    × requires home once the daemon confirms onboarding is completed
    × keeps a seeded run failing when the app ignores completed onboarding

    AssertionError: expected { acceptOnboardingLanding: true } to deeply equal { acceptOnboardingLanding: false }

    Test Files  1 failed (1)
         Tests  2 failed | 16 passed (18)

* fix(e2e): derive the accepted app shell from the daemon's onboarding config

Setup and expectation now come from one fact instead of two. The smoke reads
`GET /api/app-config` — which serves `readAppConfig(RUNTIME_DATA_DIR)`, the
running daemon's own resolved data root — and the accepted terminal state
follows from it:

  - daemon confirms onboarding is completed -> home is the only outcome, so a
    renderer that ignores a completed-onboarding config still fails the core
    release lane;
  - daemon reports a genuine first run -> the cloud sign-in landing is the
    correct place to stop, and is accepted only when it actually rendered its
    sign-in CTA and both runtime links.

The seed's postcondition is also asserted directly, right after the seeded
`tools-pack win start` where it must hold: that launch rewrites the packaged
config's `namespaceBaseRoot` to the tools-pack runtime root
(tools/pack/src/win/lifecycle.ts) and the runtime derives
`join(namespaceBaseRoot, namespace, 'data')` from it
(apps/packaged/src/paths.ts), which is byte-identical to the path
`seedPackagedOnboardingComplete` writes. A daemon that does not see the seed
there is a real data-root regression and now says so, instead of surfacing
later as an unexplained onboarding screen.

The app-shell read is deliberately taken again rather than reused: the core
profile stops the app and relaunches it through the OS protocol handler, and
that cold start inherits none of this process's environment.

    Test Files  1 passed (1)
         Tests  18 passed (18)

* test(e2e): pin the daemon onboarding-config probe (red)

PerishCode's second review on #6481: the probe converts an HTTP failure into
`onboardingCompleted: false`, which is the exact branch that permits the
onboarding landing. `status` is returned and never read, and the caller's guard
only rejects a malformed shape — so a 500, a 404, or a daemon that is not
serving yet resolves to "genuine first run" and the core smoke passes without
ever establishing the fact its shell policy depends on.

Move the probe text and its reading into the pure module verbatim, then drive
the shipped text against a fake fetch. Behavior is unchanged, so every failure
mode reads as a valid `false`.

    × refuses to answer when the daemon returns a server error
    × refuses to answer when the route is missing
    × refuses to answer when the daemon is not reachable at all
    × refuses to answer when the response carries no config
    × names the status so a failure says what could not be established
    × never lets a failed probe become permission to accept the landing

    AssertionError: expected function to throw an error, but it didn't

    Test Files  1 failed (1)
         Tests  6 failed | 21 passed (27)

* fix(e2e): fail closed when the daemon onboarding config cannot be read

The probe now reports failure structurally instead of flattening it into a
reading: a transport error, a non-2xx, or a body without `config` each come
back as `{ ok: false, status, error }`, and `packagedOnboardingCompletedFromProbe`
raises `PackagedOnboardingConfigError` for all of them. `status` is load-bearing
— only a 200 that actually carried a config object may produce a reading.

There is deliberately no default and no retry. `waitForHealthyDesktop` has
already proven the same renderer gets 200 from `/api/health`, so a failure here
is a real fault; and the only fallback value available is `false`, which is
exactly the branch that permits the onboarding landing. An unestablished fact
must be an error, never a permission.

    Test Files  1 passed (1)
         Tests  27 passed (27)

* test(e2e): pin malformed-but-successful onboarding config payloads (red)

PerishCode's third review on #6481, same defect class as rounds 1 and 2:
coercion manufacturing a value that hides the absence of one.

`config.onboardingCompleted === true` turns anything into a valid boolean, so a
200 carrying `{ config: {} }` or `{ config: { onboardingCompleted: "false" } }`
returns `{ ok: true, onboardingCompleted: false, status: 200 }`. The reader's
`typeof ... !== 'boolean'` check then passes, because `=== true` already
synthesized the boolean it was meant to require — the round-3 guarantee that a
reading must "be present" was vacuous as written.

    × refuses to answer when a 200 carries no onboardingCompleted field
    × refuses to answer when onboardingCompleted is the wrong type
    × refuses to answer when config is an array rather than an object

    AssertionError: expected { ok: true, …(2) } to match object { ok: false }

    Test Files  1 failed (1)
         Tests  3 failed | 27 passed (30)

* fix(e2e): require the onboarding reading's type instead of coercing it

The probe now returns `{ ok: false, error, status }` when a 200 carries no
boolean `onboardingCompleted`, so a missing key, a null, or the string "false"
can no longer arrive as a real `false` reading — which was the one value that
permits the onboarding landing. `Array.isArray` also rejects an array `config`,
which `typeof === 'object'` let through.

This makes the round-3 guarantee real rather than vacuous: the reader's
`typeof ... !== 'boolean'` check now runs against a value the probe declined to
synthesize.

Swept the rest of the diff for the same defect class and tightened the two
remaining truthiness reads on the policy path, so the closed direction is
structural rather than dependent on a caller staying honest:

  - `packagedAppShellPolicy` requires an explicit `false` before granting
    permission; anything else requires home.
  - `packagedAppShellSettled` requires `acceptOnboardingLanding === true`.

Everything else on the path already failed closed: the app-shell probe's
`instanceof` flags must all be positively true to settle, `asPackagedAppShellSnapshot`
type-checks every field before use and returns null otherwise, and the reader's
`status`/`ok` guards can only reject.

    Test Files  1 passed (1)
         Tests  32 passed (32)

* test(e2e): pin that a lost seed is not a genuine first run (red)

PerishCode's fifth review on #6481 — the same family, now at the level of the
scenario rather than a single expression.

The run seeds `onboardingCompleted: true` and asserts the seeded daemon reads
it back. The core profile then stops the app and relaunches it through the OS
protocol handler, which inherits none of the test process's environment. If
that cold launch resolves a different data root the seeded config is gone, the
fresh reading is `false`, and the policy calls it a genuine first run: the
landing is accepted and the smoke passes while blind to the regression it
exists to catch. Recording both readings diagnoses it; it does not fail.

Give the policy the seeded observation and leave the implementation ignoring
it, so the two cases that describe a lost seed fail.

    × never downgrades a lost seed to a genuine first run
    × keeps a lost seed failing at the shell check

    AssertionError: expected { acceptOnboardingLanding: true } to deeply equal { acceptOnboardingLanding: false }

    Test Files  1 failed (1)
         Tests  2 failed | 32 passed (34)

* fix(e2e): make the seeded onboarding state authoritative across the relaunch

Chose the seed-authoritative shape over making the core scenario unseeded: an
unseeded core run would discard the completed-onboarding boot path, which is
exactly what the first review in this chain said not to lose, and every release
workflow defaults `win_x64_smoke_mode` to `core`, so that path would stop being
exercised anywhere routine.

Once this run has seeded completion and observed the daemon confirm it, a later
`false` is a regression, never new information about the user:

  - `assertSeededOnboardingRetained` raises `PackagedOnboardingSeedError` naming
    the cold launch and the data root to check;
  - `packagedAppShellPolicy` additionally refuses permission for any run whose
    seeded observation is not an explicit `false`, so the loss cannot buy the
    landing even if the assertion were bypassed.

The spec asserts the seeded observation exists rather than coercing `'skipped'`,
which would have read as "never seeded" and handed the permissive branch the one
input it must not get.

This may surface a real packaged-runtime bug on the next Windows release run:
the protocol cold launch inherits none of the test process's environment, so if
it resolves a different data root the seed is genuinely gone. Failing there is
the correct outcome, not a reason to loosen the check.

    Test Files  1 passed (1)
         Tests  36 passed (36)

* test(e2e): pin both packaged launch scenarios as transitions (red)

PerishCode's sixth review on #6481: the run seeds unconditionally, requires the
first reading to be true, and round 5 requires that seed to survive — so
`seededObserved` is always true at the only policy call site. The policy can
only return `acceptOnboardingLanding: false`, and the unseeded unit cases
exercise a state the release smoke can never enter. This head produces a better
error and still fails, still needing `win_x64_smoke_mode: skip`.

Extract the settle loop and the phase around it so the smoke's own transition —
read the daemon fact, hold the seeded state across it, derive the policy, settle
— can be driven with only the I/O faked, then cover both scenarios the packaged
app legitimately has. `runPackagedAppShellPhase` still hardcodes the seeded
assumption, mirroring the unconditional seed, so a genuine first run fails with
the wrong error entirely:

    × settles a genuine first run on the cloud sign-in landing
    × fails a first run whose renderer never painted either surface

    PackagedOnboardingSeedError: the relaunched daemon lost the seeded
    onboarding state — check that the protocol cold launch still resolves the
    tools-pack runtime data root

    Test Files  1 failed (1)
         Tests  2 failed | 39 passed (41)

* feat(e2e): cover both packaged launch scenarios in the core Windows smoke

Adds the explicitly unseeded first-run phase the smoke never had, so the
landing-accepting path is exercised by a scenario the release lane can actually
enter rather than only by unit fixtures.

Both behaviours are real product behaviour, not a contradiction:
`shouldRouteToFirstRunOnboarding` (apps/web/src/App.tsx) keys purely on
`onboardingCompleted`, and `connectStepRuntimeReady` (EntryShell.tsx) lets
onboarding complete through cloud sign-in *or* a local CLI *or* a verified BYOK
key. So a user who finished setup and is merely signed out belongs on home,
while a run that never completed onboarding belongs on the cloud sign-in
landing.

Chose separately asserted phases over a second `test(...)`: it reuses the single
install rather than adding a second install/uninstall cycle to the release
critical path, and it keeps the two scenarios adjacent so the contrast is
legible. Phase 1 resets the daemon data root and the Electron user-data
partition, asserts the daemon reports `onboardingCompleted: false`, and requires
the landing; it then clears both again so phase 2's seed lands clean. Phase 1 is
core-only because the full profile needs its controlled updater environment from
first launch.

The settle loop and the phase around it moved into the pure module, so the
transition — read the daemon fact, hold the seeded state across it, derive the
policy, settle — is now driven by tests with only the I/O faked, for both
scenarios and their failure modes.

    Test Files  1 passed (1)
         Tests  41 passed (41)

* test(e2e): pin the real fresh-install app-config response (red)

PerishCode's seventh review on #6481: round 4 made a missing
`onboardingCompleted` fail closed, round 6 added a scenario that produces
exactly that, and they collide.

`resetPackagedRuntimeDataRoot()` deletes `app-config.json`; `readAppConfig`
returns `{}` on ENOENT (app-config.ts:769) and adds only telemetry defaults, so
a fresh install's `/api/app-config` OMITS the key rather than reporting `false`.
The probe classifies that as malformed, the reader throws, and the first-run
phase never reaches the landing it was added to assert. win.spec.ts already
documented the daemon behaviour and the probe treated it as corruption anyway.

Move interpretation off the reader and onto the scenario (the reader now returns
the raw probe outcome), then add a fixture whose body matches the daemon's
actual fresh reply — `config` with telemetry defaults and no
`onboardingCompleted`. Interpretation is still round 4's, so absent stays
collapsed into malformed:

    × settles a first run whose daemon never wrote the key
    × still fails a completed user whose key went missing

    PackagedOnboardingConfigError: packaged windows daemon onboarding config
    could not be established: daemon config carried no boolean
    onboardingCompleted (got undefined) (status=200)

    Test Files  1 failed (1)
         Tests  2 failed | 43 passed (45)

* fix(e2e): separate an absent onboarding key from a corrupt one

Chose representing absence as its own outcome over having the first-run phase
write an explicit `onboardingCompleted: false`. Writing one would make the
phase assert against a state a real fresh install never has — the same
"exercises a state the release smoke can never enter" flaw the previous review
just corrected — and would leave the probe unable to tell a fresh install from
a corrupt config anyway.

The probe now returns a discriminated outcome, and interpretation moved off the
reader and onto the declared scenario:

  | outcome           | first-run | completed-user |
  |-------------------|-----------|----------------|
  | transport-failure | reject    | reject         |
  | http-error        | reject    | reject         |
  | no-config         | reject    | reject         |
  | absent            | ACCEPT    | reject (seed)  |
  | malformed         | reject    | reject         |
  | reading           | accept    | accept if true |

`absent` is the only asymmetric row, and deliberately so: a key that was never
written is the honest state of a fresh install, while in a seeded run its
disappearance means the seed vanished — which now raises
`PackagedOnboardingSeedError` naming the cold launch rather than a generic
config fault.

The table lives in the module docblock. Seven review rounds each found a defect
of one family — a fault and an absence sharing a representation — so this is
written down as the fix for the family, not for the instance.

    Test Files  1 passed (1)
         Tests  45 passed (45)
2026-08-06 07:07:22 +00:00
AmyandGitHub 52f06da79f test: port PR 6229 acceptance coverage to main (#6272)
* test: cover updater floor and manual edit history

* test: close daemon and E2E acceptance gaps

* test: make daemon restart lifecycle budget explicit

Replace replay timing sleep with persisted checkpoint polling and allow the multi-process lifecycle test enough time for startup and cleanup.

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

* test: restore onboarding analytics stability assertion

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

* test: assert manual edit history preserves preview frame

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

* test: make manual edit shortcuts and selection assertions portable

Use the platform shortcut modifier for browser copy/paste and target the distinct inserted image before deletion so the structural tests verify the correct node.\n\nGenerated-By: looper 0.11.3 (runner=fixer, agent=codex)

* test(e2e): drop superseded manual edit acceptance cases

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

* test(e2e): drop duplicate updater floor coverage

* test(e2e): assert service-error recovery actions

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

* test(daemon): await third-boot reconciliation

Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
2026-08-06 06:32:12 +00:00
lefarcenandGitHub 2671061c47 fix(web): settle project workspace scope when bootstrap arrives late (#6504)
useProjectWorkspaceScope skipped its mount-time scope read whenever the
current render could seed from `initialScope`. That answer is recomputed
every render, but state is seeded only once, in the useState initializer.

App.tsx fills routeProjectSnapshotRef asynchronously, so a route bootstrap
that resolves after ProjectView mounted flips `initialScopeCanSeed` on an
already-running hook. Because it is an effect dependency, the effect re-ran,
aborted the in-flight GET /api/projects/:id/workspace-scope, and returned
without fetching or setting state — pinning `loading: true` forever.

Nothing could recover it: with no resolved scope the workspace invalidation
SSE never subscribes, so no event could bump the revision and retry. The
stuck `loading` reaches ProjectView as `workspaceContextReadOnly`, which
makes `viewerOnly` true and renders the whole workspace as a read-only
shared project — dead composer, disabled create actions — on a project the
user owns.

Key the skip on whether state was actually seeded at mount, which is what
the early return always assumed. A late bootstrap scope is now ignored and
the mount-time read settles normally.
2026-08-06 06:24:19 +00:00
Ray XiandGitHub 79aeb9977a fix(landing): lock downloads to stable R2 metadata (#6503) 2026-08-06 06:24:13 +00:00
2b8aa8c457 feat(design-systems): add structured runtime contract (#6456)
* feat(design-systems): add structured runtime contract

* chore: retrigger CI for PR 6456

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: mrcfps <mrc@powerformer.com>
2026-08-06 06:08:21 +00:00
open-design-crew[bot]GitHubopen-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>elifive555555
f2a7568dae feat(desktop): accept opendesign://workspace/open focus hand-off deeplink (#6410) (#6499)
* feat(desktop): accept opendesign://workspace/open focus hand-off deeplink

The cloud device-activation page will fire this after a client-originated
sign-in completes in the browser, so the user lands back in the desktop app
instead of staying on the web. The deeplink is payload-free: login state
already arrives through the daemon's vela login polling, so handling it only
refocuses the client.

* fix(desktop): route the deeplink hand-off focus through the runtime

`focusPrimaryWindow` grabbed `BrowserWindow.getAllWindows()[0]`, which is the
hidden, non-focusable desktop-pet window in a running app (and the splash on a
packaged cold start) — never the main window. For `workspace/open` the focus is
the entire payload, so the hand-off resolved to a silent no-op while still
reporting `ok: true`. Route it through the runtime's own splash-aware `show()`
instead, and drop the broken helper.

Also tag the focus outcome with `workspace_open_focus` so the completion log
stops claiming an invite continuation that never touched the daemon, and pin the
accept boundary of `isWorkspaceOpenDeeplink` with a rejection matrix.

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

* fix(desktop): restore minimized window on hand-off and guard the focus call

The revealed path of DesktopRuntime.show() only called window.show() +
window.focus(), which leaves a minimized client in the Dock — turning the
deeplink hand-off (whose entire payload is the bring-to-front) into a no-op.
Route it through ensureWindowVisible, which restores before focusing.

The workspace/open branch also called deps.focus outside any guard; a throw
during runtime teardown would escape continueInviteFromUrl's documented
no-throw contract as an unhandled rejection. Catch it and report a structured
focus_failed outcome instead.

---------

Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
2026-08-06 03:50:38 +00:00
AmyandGitHub 5a2e56101e test(e2e): restore workspace-aware P0 coverage (#6445)
* test(e2e): restore workspace-aware P0 coverage

* test(e2e): await collaboration unload and event readiness

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

* test(e2e): slim multi-client collaboration P0

* ci(e2e): split workspace editor P0

* test(e2e): avoid ambiguous upload assertion
2026-08-06 03:00:27 +00:00
Marc ChanandGitHub fef0ddf88e ci: auto-rerun once on runner/spot cancel (#6453)
* ci: auto-rerun once when runner/spot cancel kills ci jobs

Add rerun.atom.yml as a trusted workflow_run consumer that requests one
`gh run rerun --failed` when completed pull_request/merge_group ci runs
lose leaf jobs to infra cancel (shutdown signal / cancelled runner),
while skipping ordinary assertion failures, stale heads, and ejected
merge-queue entries.

* fix(ci): grant rerun atom read scopes and refuse mixed failures

Add checks/pull-requests read permissions so the helper can fingerprint
infra cancels, and require every non-success leaf to be infra-classified
before authorizing gh run rerun --failed.

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

* fix(ci): require open PR for live-head stale gate

Stop treating a same-repo branch tip as live-PR evidence. Closed PRs can
leave the source branch at the failed run SHA with no open association;
query open pulls by repository-qualified head ref instead, and cover the
closed-PR + unchanged tip shape in self-check.

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

* fix(ci): require infra marker for cancel rerun

Bare cancelled jobs without shutdown/spot annotations are ordinary
(manual cancel) and must not authorize gh run rerun --failed.

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

* fix(ci): ignore aggregate gate jobs in infra-cancel classify

Validate workspace and Runtime summary fail as always() mirrors of leaf
outcomes and never carry runner/spot markers. Counting them as ordinary
blocked pure infra-cancel reruns whenever the workflow still reached the
gate. Exclude those names from classification and cover the shape in
self-check.

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

* fixer: address PR #6453 follow-up items

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

* fix(ci): refuse mixed ordinary+infra job annotations

Jobs that record both a real failure annotation and a later runner
shutdown marker must classify as ordinary, not pure infra-cancel, so
gh run rerun --failed cannot auto-retry assertion failures.

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

* fix(ci): paginate check-run annotations before infra classify

GitHub defaults annotations to 30 results; a later ordinary failure must still
block gh run rerun --failed. Walk pages like paginate_jobs and cover the split
with a multi-page request fixture.

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

* fix(ci): drop tracked pycache from infra-cancel helper

PerishCode BLOCKING on #6453: remove the accidental CPython 3.14
bytecode under .github/scripts/__pycache__/ and ignore __pycache__/
plus *.py[cod] so local self-check/compile output stays out of git.

* fix(ci): open-PR merge_group freshness; ignore warning annotations

Merge-group infra-cancel retries no longer require the synthetic head to
still sit in the merge queue — workflow_run completed fires after required
checks already ejected it. Freshness is an open PR still associated with
the run (commit→pulls or gh-readonly-queue pr-N ref).

Warning-level annotations (e.g. scopes.ts full-plan fallback) no longer
count as ordinary failures that suppress pure infra-cancel retry.

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

* fix(ci): match merge_group PR heads and arm rerun e2e Vitest

Compare the live open PR head with the PR head SHA encoded in the
merge-queue branch before authorizing infra-cancel retry, and enroll the
rerun atom surfaces in scopes so PR CI runs the topology self-check.

Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
2026-08-06 01:38:59 +00:00
open-design-bot c7dbe83788 chore(events): append pr_merged 2026-08-05 22:19:51 +00:00
open-design-bot 7a1c34f569 chore(contributors): +12pts for @lefarcen (PR #6491) 2026-08-05 22:19:50 +00:00
lefarcenandGitHub ace01bcdd6 Merge pull request #6491 from nexu-io/backmerge/v0.18.0
chore(release): back-merge release/v0.18.0 into main
2026-08-06 06:19:47 +08:00
lefarcen 876cb4d054 chore(release): back-merge release/v0.18.0 into main
Brings back the commits that landed directly on the release branch and exist
nowhere else: #6480 (guard the windows e2e report download so
win_x64_smoke_mode: skip is usable on the stable lane), #6452 (retry NSIS
install and fail the setup step loudly), #6440 and #6437 (release/v0.18.0
auto-build defaults, both scoped by their own github.ref_name conditions),
#6435 (the amr_profile input that enables the Workspace Team transport), plus
the release-cut chores.

apps/packaged/package.json keeps main's 0.16.2 rather than taking the branch's
0.18.0: this repo advances main's version through its own
'chore(release): bump main to X.Y.Z ahead of stable' PRs (#6000, #5661, #5441),
never through a back-merge.
2026-08-06 06:17:41 +08:00
lefarcenandGitHub 30c427ce0a fix(release): allow a skipped windows smoke on the stable lane (#6480)
release-stable.yml offers win_x64_smoke_mode: skip, but choosing it makes the
release impossible to finish. The windows smoke uploads its e2e report with
if-no-files-found: warn, so a skipped smoke leaves the report directory empty
and publishes no artifact; finalize then downloads that artifact
unconditionally and fails — after every platform has already built, signed and
notarized, and after the pre-flight tag check has passed. Twenty-five minutes
of work is thrown away for a report nothing downstream reads.

Guard the download on the same condition that gates the smoke itself, matching
how the linux report is guarded by its build result. The mac report stays
unconditional because the mac smoke has no skip switch.
open-design-v0.18.0
2026-08-06 00:16:15 +08:00
0dde0e5043 fix(landing): simplify Codex plugin install entrypoint (#6474)
* fix(landing): simplify Codex plugin install entrypoint

* fix(landing): handle install guide fetch failures

---------

Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
2026-08-05 13:30:43 +00:00
open-design-release-bot[bot]GitHubopen-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>bone3deep1962-collab
65e1b2a3c5 feat(analytics): track recoverable task outcomes (#6411) (#6473)
(cherry picked from commit e6aeb38da6)

Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
2026-08-05 21:18:16 +08:00
c670aff42b docs(release): add 0.18.0 release notes (#6469) (#6472)
* docs(release): add 0.18.0 release notes

* docs(release): adopt final 0.18.0 product copy for title and intro

(cherry picked from commit ae2a0b0101)

Co-authored-by: lefarcen <935902669@qq.com>
2026-08-05 20:48:12 +08:00
63e54e298c fix(release): bake the workspace-team gate into stable and preview builds (#6468) (#6471)
release-stable.yml and release-preview.yml never set OPEN_DESIGN_AMR_PROFILE
or OD_VELA_WEB_URL. Neither has a default in tools-pack, so velaWebUrl reached
the packaged config as undefined, workspaceTeamTransportEnv returned an empty
record, and the four vela transports were never injected into the daemon spawn
env. Both channels therefore shipped clients with Workspace Team silently
dormant — the build succeeds, the app installs and starts, and the absence only
surfaces once a package is in a user's hands.

beta and prerelease already inject the pair from their amr_profile input, which
is why prerelease validation looked correct: the artifact under test had the
feature and the stable artifact promoted from it would not have.

Stable and preview are production channels, so the pair is pinned instead of
dispatched — there is no legitimate stable build aimed at the test backend.
Naming prod explicitly is a no-op for AMR itself (the daemon already defaults to
prod in integrations/vela-profile.ts); it exists to satisfy the profile half of
the gate. A fork or PR build without the secret leaves the origin empty, so the
transports stay dormant rather than half-enabled.

(cherry picked from commit 3371ce4b2a)

Co-authored-by: lefarcen <935902669@qq.com>
2026-08-05 20:45:46 +08:00
Marc ChanandGitHub 9483329ecd ci: shard web tests and retain visual failure artifacts (#6416)
* ci: shard web tests and retain visual failure artifacts

Split the 6m web workspace job into two vitest shards and keep
Playwright trace/screenshot on visual failure for diagnosis.

* test(e2e): pin visual-pr-capture retain-on-failure path

Assert playwright_visual uploads keep e2e/ui/reports/visual-test-results
for both visual-pr-capture and visual-ci artifacts so report-consumed
diagnostics cannot silently regress.

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

* test(e2e): pin web shards and baseline visual diagnostics

Lock the two-way web_workspace_tests vitest matrix in the CI topology
contract and upload retain-on-failure traces from visual-baseline.yml,
the second consumer of playwright.visual.config.ts.

Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
2026-08-05 11:48:10 +00:00
d771c1ca5f feat(daemon): export execution diagnostics (#6433)
* feat(daemon): export execution diagnostics

* fix(daemon): preserve observed execution diagnostics

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
2026-08-05 11:45:47 +00:00
e6aeb38da6 feat(analytics): track recoverable task outcomes (#6411)
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
2026-08-05 11:41:30 +00:00