* 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>
* [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)
* 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>
* fix(daemon): locate URL preview injection points structurally
Three injectors on the URL-load preview path found their insertion point
with a raw text match: the first `</body>` for the scroll / selection /
snapshot bridges, the first `<head>` for the sandbox / focus / redirect /
observability guards, and the first `<head>` again for the containment
`<base>`. Those tags are also perfectly ordinary *content* — any
prototype that builds an HTML document (a print window, an email
template, a `srcdoc` payload) writes them into a script string or a
`data-` attribute. So the injected markup landed inside the author's
string, which ended their script early — or closed their attribute
early, since the injection carries quotes — and rendered the remainder
as page text, with no console error to explain it.
All three now share `findRealTagOffset`, which walks tag by tag instead
of character by character and skips the places a tag-looking run of text
is not a tag: comments and markup declarations, raw-text element content
(the full HTML5 set, `noscript` included since the preview iframe always
runs with scripting enabled), attribute values, and `<template>`
content, which the tree builder keeps out of the document so an
injection placed there would silently never run. `endOfTag` gives the
tag's real extent, so a `>` inside a quoted value cannot split it, and
the `<head>` / `<html>` / `<base>` patterns carry a tag-name boundary so
`<head[^>]*>` no longer matches `<header>`.
This became reachable in 0.21.0: before #7336 an artifact needing the
sandbox shim or focus guard was forced onto the srcDoc path, whose
`injectBeforeBodyEnd` already anchored on the real `</body>`. Serving
those guards over URL-load instead exposed the daemon copies, which
never got the same treatment.
Fixes#7410
* fix(daemon): require a real end-tag boundary to leave raw text
Review catch on #7421. The raw-text skip resumed at the first `</name`
substring, but the tokenizer only leaves raw text when that name is
followed by whitespace, `/`, or `>`. A longer name starting with it is
still character data, so:
<script>const doc = "</script-template><body>slip</body>";</script>
put the scan back into the author's string at `</script-template>`, and
the very next `</body>` it handed back came from inside that string —
recreating the corruption this branch exists to prevent. Measured against
parse5: the scan returned offset 49, the parser says 77.
`findRawTextClose` now walks candidates until one is followed by a real
boundary character, and reports none when the element never closes, which
keeps an unterminated end tag from closing it. The route fixture covers
both a `<script>` prefix and an RCDATA (`<textarea>`) one.
* fix(daemon): follow the tokenizer's template and script-escape states
Review catch from @Siri-Ray. Two state gaps let the scan resume inside
author content and hand back a boundary from there.
Template depth was counted with a regex, so a `</template>` written inside a
nested script string ended the template early:
<template><script>const doc = "</template><body>slip</body>";</script></template><main>real</main></body>
scanner 64, parse5 110
`skipTemplateContent` now walks the content with the same skip rules the
main scan uses. This also corrects what the previous comment claimed: being
fooled did *not* degrade to -1, it returned a wrong offset.
Script data has escape states the other raw-text elements do not. After
`<!--` a nested `<script` moves the tokenizer to double-escaped, where
`</script>` steps back out instead of closing:
<script><!-- const open = "<script>"; const doc = "</script><body>slip</body>"; //--></script>
scanner 82, parse5 123
`findScriptClose` tracks those states; other raw-text elements keep the
simple boundary walk. Route fixture covers both shapes.
* fix(daemon): treat CDATA as markup only in foreign content
Both reviewers landed on the same gap. Declarations were all skipped to the
next `>`, but foreign content is the one place `<![CDATA[ … ]]>` is real
markup, so that `>` can still be inside the section:
<svg><![CDATA[label > </body>]]></svg><main>real</main></body>
scanner 34, parse5 67
The scan resumed inside the CDATA and returned the `</body>` it holds, which
put the bridge in the SVG as inert text and mutated the authored content —
the failure this locator exists to prevent.
`skipForeignContent` walks an `<svg>`/`<math>` subtree with the same skip
rules and consumes `<![CDATA[` through `]]>`, tracking HTML integration
points (`<foreignObject>`, `<desc>`, …) where the parser is back in HTML and
`<![CDATA[` is a bogus comment again. The whole subtree is skipped either
way: no document-level boundary can live inside one.
Checked against parse5 on six shapes — CDATA in svg and in math, an
integration point, nested svg, a self-closing `<svg/>`, and plain svg with no
CDATA — all matching. Route fixture covers the first three.
* fix(daemon): PLAINTEXT has no way out
Found by sweeping the scanner against parse5 across tokenizer states rather
than waiting for the next review round.
`<plaintext>` switches the tokenizer to PLAINTEXT, and unlike every other
raw-text element there is no exit: all input after it is character data,
`</plaintext>` included. The scan honoured that close tag and resumed, so in
`<body><plaintext></body></plaintext><main>x</main></body></html>` it handed
back the trailing `</body>` (offset 56) where the parser reports none. The
bridge was then spliced into inert text and never ran at all.
Reporting no close is what keeps the scan out; the caller appends instead,
where the bridge still runs.
The same sweep found three more offset differences from parse5 that are
*not* defects — `<!-->`, `<!--->`, and a `<![CDATA[` outside foreign content
all place the injection correctly and leave the document unchanged, so the
offset difference is the parser's own bookkeeping. One genuine limitation
stands: a doctype with an internal subset (`<!DOCTYPE html [ … ]>`) puts the
bridge in `<head>` rather than at the end of `<body>`. It still runs and
nothing is corrupted, and matching it would take insertion-mode tracking
this scanner deliberately does not do.
* fix(daemon): refuse to guess a boundary through CDATA in foreign content
Three review findings converge on the same construct, and the honest answer
is to stop modelling rather than model harder.
Whether `<![CDATA[` opens a section or is a bogus comment depends on the
adjusted current node's namespace: an integration-point element is itself
still SVG/MathML so CDATA directly inside it is a section, `annotation-xml`
is an integration point only when its `encoding` is `text/html` or
`application/xhtml+xml`, and foreign content nested in a `<template>` follows
the same rules. Modelling that correctly is the HTML tree-construction
algorithm, which this scanner deliberately is not.
So it refuses: any `<![CDATA[` inside `<svg>`/`<math>` ends the scan with "no
boundary", the caller appends, and the injection still runs with every
authored byte intact. Guessing is the only outcome that corrupts, which makes
it the one worth giving up precision for. `skipTemplateContent` now routes
foreign content through the same skip so a template cannot slip past it.
Measured before choosing: across the 2544 HTML files in this repo, `<svg>`
appears in 178 and `<![CDATA[` in **none**. Precise placement is kept for
every one of those 178; the conservative path is reserved for a construct the
product's own content never produces.
Route fixture now carries all three reviewed shapes — CDATA in a
`<foreignObject>`, in a template's `<svg>`, and in an unencoded
`annotation-xml` — asserting each section survives byte for byte and the
bridge lands after the document.
* fix(daemon): keep double-escaped script state across `-->`
Review catch. `-->` returns *escaped* script data to normal script data, but
while double-escaped the tokenizer stays where it is. Clearing both flags
there accepted the next `</script>` as the element close:
<script><!--<script>--></script><body>slip</body></script><main>real</main></body>
scanner 42 (the `</body>` still inside the script), parse5 75
Checked against parse5 on all three transitions — double-escaped across
`-->`, plain escaped across `-->`, and double-escaped leaving through its own
`</script>` — all matching. Route fixture carries the first.
* fix(daemon): close the token-shape gaps an audit found, and revert 6136760
I ran a fresh adversarial audit against parse5 rather than keep taking
findings one round at a time. It found six more, and it found that my own
previous commit was wrong.
**Reverting 6136760.** That commit implemented a review claim that `-->`
keeps the tokenizer double-escaped. The spec says the opposite: in the
script-data-double-escaped-dash-dash state, `>` switches to the plain script
data state, so `-->` leaves both escape levels. Keeping double-escaped runs
the scan *past* the element's real close:
<script><!--<script>--></script><p a="</script></body>">q</p><div>x</div></body>
with 6136760: 65 (inside the attribute value) correct: 50
My verification of that claim was insensitive — the document I checked was a
reopened-body case where both behaviours return the same offset, so I got a
false confirmation. The audit's 59k-script differential settles it: the
reverted form closes early 0 times and late 219 times; restoring this line
drives the late count to zero.
The route fixture built on that claim is removed with it, since after the
revert that `</body>` really is a boundary.
**New fixes, each with an audit reproduction:**
- End-tag-open on a non-ASCII letter (`</ note…`, `</3`, `</"`) is a bogus
comment to the next `>`. All three walkers scanned inside it, so author
prose was read as markup — and in foreign content a `</svg>` inside the
bogus comment ended the subtree early.
- Comments close on `--!>` as well as `-->`, and `<!-->` / `<!--->` are
already closed. `indexOf('-->')` walked past the real end into author text
and matched a `-->` inside a script string.
- Tag names run to whitespace, `/` or `>`. Matching `[a-z][a-z0-9]*` made
`<iframe-x>`, `<style-guide>`, `<title-bar>` inherit raw-text handling, so
the scan hunted for `</iframe` and found the one in an author string.
- A `/` ending an unquoted attribute value is not a self-closing solidus
(`<svg data-href=http://x/>`). 6,666 false positives in a 40k-tag
differential, always toward entering a subtree that should be skipped.
- Injecting before a leading DOCTYPE puts the parser in quirks mode and the
DOCTYPE is dropped, silently changing the artifact's box model. Both
fallbacks now insert after it.
Audit re-run of the same 64k-document corpus against a model carrying these:
corruption 585 → 0, missed boundaries 4,937 → 29. Locally: 3000 adversarial
and 2433 real documents, 0 broken.
* fix(daemon): keep the lowercase shadow index-aligned, and two boundary details
Three more from review, all mechanism-level rather than reproduced-by-me:
**The lowercase shadow was not index-preserving.** Every offset the scan
takes from `lower` is used against `html`, but `String.prototype.toLowerCase`
is not length-preserving — U+0130 lowercases to two code units — so a single
`İ` anywhere ahead of a raw-text element shifts every subsequent offset.
I could not construct a failing document quickly, which is exactly why this
is worth fixing on the mechanism: it is luck, not safety. Tag names are
ASCII, so `asciiLower` lowercases only A–Z and the shadow stays aligned.
Verified with `İ` before, inside, and in an attribute ahead of the boundary,
1–8 copies: all aligned.
**Boundary classes now use HTML's ASCII whitespace**, not JavaScript `\s` —
`\s` matches U+00A0 and friends, which the tokenizer does not treat as a tag
boundary.
**A doctype may be preceded by comments.** `prependAfterDoctype` only
skipped whitespace, so `<!-- c --><!doctype html>` still had the payload
inserted ahead of everything, which forces quirks mode. Confirmed via
`parse5.parse().mode`: quirks before, no-quirks after.
* fix(daemon): three foreign-content and delimiter details from review
I went through all 18 open review threads against this head rather than
taking them one at a time. Thirteen were still current; of those, three were
real and the rest already land the bridge correctly (their offsets differ
from parse5's, which is not the same thing as being wrong — parse5 records
which token closed an element, and several of these documents reopen the
body).
The three:
- The HTML raw-text list was applied inside foreign content. `<svg><script>`
is an ordinary SVG element, so `<` in it is markup and a CDATA section
inside it is still a section; treating it as HTML raw text hid the CDATA
and let the scan resume inside it. The list is simply not consulted there
now — the subtree is skipped either way.
- An unquoted attribute value may follow whitespace after `=`
(`<svg data= http://x/>`), so whitespace alone no longer ends the pending
value state. Without this the trailing `/` read as a self-closing solidus.
- Carriage return is a tag-name delimiter, so `<script\r>` is a script.
All 18 reproductions now place the bridge inside `<body>` with the document
otherwise unchanged. Corpora unchanged at 0 broken (3000 adversarial, 2433
real).
* docs(daemon): record why this is a scanner and not a parser
Every reviewer so far has reasonably asked why this file re-implements
tokenizer states instead of calling a parser. The answer, with the numbers
behind it, now lives next to the code so it does not have to be re-derived
each round.
Covered: the measured correctness of this scanner against parse5,
htmlparser2 and a plain text match on three corpora; the 6-8x cost;
why parse5's Tokenizer alone is wrong here (raw-text mode is driven by tree
construction, so there is no cheap SAX shortcut); why a lean custom
treeAdapter buys nothing (the cost is tokenization, not tree building); and
why `sourceCodeLocationInfo` cannot be turned off.
Also records the finding that changed my own recommendation: parse5 answers
"which token closed the body", not "where may I splice". On 15,000
structure-agnostic tag-soup documents the scanner found a valid insertion
point wherever one existed, and never failed where parse5 succeeded, while
parse5 reported no location for ~90 per 3000 documents that did have one.
Adopting it would trade tokenizer-state code for fallback code, not remove
hand-written logic.
* fix(daemon): the append fallback did not work, and two foreign/comment details
Three more from review, one of which was a false claim in my own test.
**The append fallback was wrong, and the test said otherwise.** When the scan
reports no boundary the caller appended — but the usual reason there is no
boundary is that the document ends inside a construct that swallows what
follows: `<plaintext>` never leaves PLAINTEXT, an unterminated comment or
script runs to EOF. Appended markup becomes text there and the bridge is never
an element at all. My `plaintext-tail` spec asserted only the marker's byte
position and its comment claimed the bridge "still runs" — it did not. The
fallback now enters at the top of the document instead, which costs the
end-of-body placement but keeps the bridge live, and every fallback assertion
parses the response and requires a real element (via cheerio, already a daemon
dependency). Verified live for `<plaintext>`, an unterminated script, and an
unterminated comment.
**HTML integration points are back.** Blanket "no raw-text list inside foreign
content" over-corrected: a `<script>` inside `<foreignObject>`, `<desc>` or
`<title>` is HTML raw text again, so `<svg><foreignObject><script>` holding a
`</svg><body>slip</body>` string had the scan resume inside it.
`annotation-xml` stays out — it qualifies only with an `encoding` of
`text/html` or `application/xhtml+xml`, and CDATA is refused there anyway.
**`prependAfterDoctype` walks leading comments with `endOfComment`** rather
than its own `-->`-only regex, so `<!-- license --!><!doctype html>` no longer
falls through to prepending ahead of the doctype and forcing quirks mode.
* fix(daemon): follow the adjusted current node, and correct three stale claims
Four findings from review, all reproduced before fixing.
`skipForeignContent` tracked HTML integration points with a depth counter, which
cannot express re-entry: `<svg><foreignObject>` returns to HTML, but a `<math>`
beneath that foreignObject is MathML again and its children are foreign. It is
now a stack of `{ name, childNs }` frames. `childNs` follows the tree builder:
SVG `foreignObject`/`desc`/`title` and the MathML text integration points
(`mi`/`mo`/`mn`/`ms`/`mtext`) return to HTML, and `annotation-xml` does so only
when its `encoding` names an HTML type — read off the tag rather than assumed.
That makes CDATA answerable instead of refused. `<![CDATA[` is character data to
`]]>` outside HTML, and a bogus comment inside an integration point. The three
documents review supplied now resolve to the same offset parse5 does (95, 133,
144). `skipTemplateContent` already delegated `<svg>`/`<math>` here, so the
template case was the same defect rather than a second one.
`isEndTagBoundary` omitted U+000D. The opening-tag scanners and the
`body`/`head`/`base` patterns already carried `\r`; this predicate did not. On
512 generated documents whose raw text closes with `</tag\r>`, the previous
commit is wrong on 256 and this one on 0. The failure is degradation, not
corruption: the unclosed raw text swallows the document, the scan reports no
boundary, and the bridge falls back into `<head>`.
Three claims in the rationale block were false at this head and are corrected.
It cited a differential oracle and a `pnpm guard` check as CI protection; both
live in the follow-up that extracts this locator into `packages/contracts`, and
naming them here described a net that does not exist on this branch. It also
said the fallback appends (it anchors on `<head>`, then `<html>`, then the
doctype) and that CDATA is refused. The block now states only what this commit
enforces, and names the parse5 sweep as the development harness it is.
The existing foreign-content spec asserted the old refusal. Its first section is
`<foreignObject><![CDATA[x >` — HTML, so a bogus comment, and the `</body>`
after it really does close the body. The scan stops there, as the tree builder
does; the spec now asserts a live bridge in `<body>` instead of byte identity
for that one section.
Validation: `foreign-namespaces.html` and the extended `cr-delimiter.html` are
red on 8da5b5f and green here (64 route specs pass). `pnpm guard` and daemon
typecheck exit 0. Against parse5 under the outcome invariant: 2,433 repository
HTML files, 3,000 generated adversarial documents, and ~17,000 tag-soup
documents across six seeds — 0 documents broken, and 0 where parse5's offset
worked and the scanner's did not.
* fix(daemon): honour the foreign-content breakout rules, and keep a BOM at byte zero
Two more findings from review, both reproduced as bridge loss — the same class
this PR exists to close — before fixing.
The dispatcher's foreign-content exceptions depend on the token as well as the
adjusted current node, and the namespace stack was reading only the latter. A
breakout start tag (`<p>`, `<div>`, `<font color>`, …) pops the SVG/MathML
element and is reprocessed as HTML, so the foreign subtree ends at the tag
rather than at a matching close; `<mglyph>` and `<malignmark>` go the other way
and stay in MathML beneath a text integration point. Reading both by namespace
alone put the scan back inside author content:
<svg><p><script>const x = "</svg><body>slip</body>";</script>…
<math><mi><mglyph><![CDATA[x > </math><body>slip</body>]]>…
On the previous commit each selected the authored `</body>`, spliced the bridge
into the string literal, and reparsing found zero live bridge elements. Both now
resolve to the same offset parse5 does, as do `<malignmark>` and `<font>` with
and without a presentational attribute.
`prependAfterDoctype` skipped code points `<= 32`, so a leading U+FEFF was not
skipped, the doctype match failed, and the payload went in front of the BOM.
That demotes the BOM from encoding signature to character token, which puts a
character before the doctype and drops the document into quirks mode. The BOM
now stays at byte zero and every offset is measured after it.
Validation: `foreign-breakout.html` and `bom-plaintext.html` are red on 4b4076a
and green here (66 route specs, 79 with the containment suite). `pnpm guard` and
daemon typecheck exit 0. Against parse5 under the outcome invariant: 2,433
repository HTML files, 3,000 generated adversarial documents, and ~17,000
tag-soup documents across six seeds — 0 broken, 0 where parse5's offset worked
and the scanner's did not.
The BOM spec reads the response through `arrayBuffer()` and a decoder with
`ignoreBOM`, because `Response.text()` runs a UTF-8 decode that strips the very
byte under test; read that way the spec passed against the unfixed code. Its
fixture also carries no `<head>`/`<html>`, so the earlier fallbacks miss and the
doctype branch is actually the one exercised.
* fix(daemon): read parsed attributes, not attribute-like text in a value
Review caught this in the previous commit's own new code, and it is the same
mistake this file exists to prevent, one level down: `tagHasAttribute` and
`tagAttributeValue` ran a regex over the raw start tag, so text inside a quoted
value read as structure.
`<svg><font data-note=" color=red"><![CDATA[x > </svg><body>slip</body>]]>` has
one attribute, not two. The scanner saw a presentational `color`, applied the
`<font color>` foreign-content breakout, resumed under HTML rules inside the SVG
subtree, and selected the authored `</body>` out of the CDATA section — offset
103 against parse5's 143, with the bridge spliced into author content and no
live element left. The `encoding=` spelling had the same hole and could turn a
plain `annotation-xml` into an HTML integration point.
Both helpers are replaced by `parseTagAttributes`, which walks the tag once
using the tokenizer's quote and value states — the same ones `endOfTag` uses —
and returns real names and values, first occurrence winning as the parser does.
Values are not trimmed: `encoding` is compared to `text/html` exactly, so a
padded value is not a match and must not be smoothed into one.
Validation: `quoted-fake-attrs.html` is red on edfb1bf and green here (67 route
specs, 80 with the containment suite). Attribute parsing is checked directly
against nine tag shapes — quoted fakes, unquoted values, an empty value, a
duplicate name, mixed case, single quotes, and a self-closing tag with none.
`pnpm guard` and daemon typecheck exit 0. Corpora unchanged: 2,433 repository
HTML files, 3,000 adversarial documents, and tag-soup fuzz across four seeds,
0 broken and 0 scanner-only failures.
* fix(daemon): resolve character references in an attribute value before matching
The tokenizer resolves references in attribute values before the tree builder
compares them, so `encoding="text/html"` makes `annotation-xml` an HTML
integration point — its `<script>` is HTML raw text. Reading the source
spelling kept the scan in MathML, took the authored `<body>` as a foreign
breakout, and spliced the bridge into the script string: offset 122 against
parse5's 181, with no live bridge left.
`decodeAttributeValue` resolves numeric references with or without the trailing
semicolon, as the tokenizer does inside a value, and named ones only when
terminated by `;` — an unterminated name followed by an alphanumeric is an
ambiguous ampersand and stays literal.
The named table has three entries, and that is complete rather than partial for
this comparison: the accepted encodings are letters plus `/`, `+` and `.`, and
HTML has no named reference for an ASCII letter, so nothing outside numeric
references and `sol` / `plus` / `period` can spell one.
Validation: the encoded shape is added to `quoted-fake-attrs.html`, which is red
on 3caeed07 and green here (67 route specs, 80 with the containment suite).
Decoding is asserted against nine values, including the unterminated named form
that must stay literal and a `�` that must not decode. The literal
`encoding`, quoted-fake `encoding`, and quoted-fake `color` cases all still
resolve as before. `pnpm guard` and daemon typecheck exit 0; corpora unchanged
at 0 broken and 0 scanner-only failures.
* refactor(daemon): keep attribute parsing separate from decoding
Not a review finding — a self-audit of the code the last two commits added,
prompted by the fact that both of those commits were fixes to the commit before
them. I ran the new parser against parse5 on generated tags the way the scanner
itself is checked.
Attribute *splitting* matched exactly: 8,000 tags with mixed quoting, whitespace
around `=`, valueless attributes, duplicate names, mixed case and a trailing
solidus — 0 name mismatches, 0 value mismatches wherever no character reference
was involved. The only divergence was decoding, from the deliberately small
named table. That is fine for the one comparison this file makes and wrong as a
general contract, and `parseTagAttributes` was returning decoded, lowercased
values as if it were general.
So the two concerns are separated. `parseTagAttributes` returns names lowercased
and values verbatim — a contract the differential above actually establishes.
`annotationXmlEncodingIsHtml` owns the decode-and-compare, which is where the
completeness argument for the three-entry named table belongs.
Numeric references also now follow the tokenizer's end state: null, out of
range, and lone surrogates become U+FFFD rather than staying literal. None of
those can spell an accepted encoding, so this changes no outcome here; it means
the function does what its name says.
Validation: `encoding-near-misses.html` pins the two shapes that must *not*
become integration points — a padded ` text/html ` and an unterminated
`&solhtml` — and both CDATA sections stay character data. 68 route specs, 80
with the containment suite. `pnpm guard` and daemon typecheck exit 0; corpora
unchanged at 0 broken and 0 scanner-only failures.
* fix(daemon): restore the scanner that the previous commit reverted
03ea0540 committed `origin/main`'s copy of this file, deleting the whole
scanner. Not a review finding and not a design change — my own mistake.
I had a background script running the neighbouring flaky suite against both
sides of the change, which swaps this one file between the branch and `main`
copies between rounds. I kept editing the same file while it ran, so `git add`
picked up whichever copy the script had just written. The test-file half of that
commit landed correctly; only the source was clobbered.
This restores the file to 57233b5f and re-applies the separation of parsing from
decoding that 03ea0540 was supposed to carry:
* `parseTagAttributes` returns names ASCII lowercased and values verbatim —
the contract the parse5 differential actually establishes (8,000 generated
tags, 0 name mismatches, 0 value mismatches absent a character reference).
* `annotationXmlEncodingIsHtml` owns the decode-and-compare, which is where
the completeness argument for the three-entry named table belongs.
* Numeric references follow the tokenizer's end state: null, out of range and
lone surrogates become U+FFFD instead of staying literal.
Validation after the restore, not carried over: daemon typecheck and `pnpm
guard` exit 0; 68 route specs and 81 with the containment suite pass; against
parse5 under the outcome invariant, 2,433 repository HTML files, 3,000
adversarial documents and tag-soup fuzz on two seeds are all 0 broken with 0
scanner-only failures.
* fix(daemon): walk a tag in the tokenizer's states, and match named references exactly
Two findings from review, both reproduced first, plus one my own fuzzing found
while fixing them.
`endOfTag` decided whether a quote opened a quoted value by looking at the last
non-space byte, and `isSelfClosingTag` carried a copy of the same heuristic.
`=` is also an ordinary character of an unquoted value, so `<head data==">` — a
complete start tag, since the second `=` opens an unquoted value and the quote
belongs to it — was read as opening a quoted value. The walk then ran to the
next quote in the document and returned a `>` from inside a script string, and
the route rewrote the author's source at that offset. That is the corruption
class this PR closes.
The heuristic is gone. `scanTag` walks a tag once in the tokenizer's own states
and returns the three things callers need — the closing `>`, whether the solidus
really self-closes, and the parsed attributes — because all three depend on the
same state. `endOfTag` is now a one-line wrapper, `isSelfClosingTag` and
`parseTagAttributes` are deleted, and the three call sites each scan once
instead of three times.
Named character references are case-sensitive. `/` is one and `&SOL;` is
not, but the lookup lowercased the name, so `encoding="text&SOL;html"` turned
`annotation-xml` into an HTML integration point the parser never made one:
offset 117 against parse5's 168, bridge dead. The lookup is exact now; the MIME
comparison after it stays ASCII case-insensitive, which is what the spec says.
Fuzzing the new walker against parse5 then turned up a third case, not reported:
an `=` where an attribute name should start is the name's first character, per
the unexpected-equals-sign-before-attribute-name rule — not a value separator.
Reading it as one consumed the following quote as a delimiter and, again, ran
the tag past its real end.
Validation: `unquoted-equals-tag.html` and `encoding-case.html` are red on
a0c1d143 and green here (70 route specs, 83 with the containment suite).
`scanTag` matches parse5's start-tag extent on 57,902 generated tags across five
seeds — 0 mismatches — after the unexpected-equals fix, which had 12. `pnpm
guard` and daemon typecheck exit 0; corpora unchanged at 0 broken and 0
scanner-only failures.
* fix(daemon): separate tokens on HTML whitespace only, not the whole C0 range
`scanTag` treated every code point at or below U+0020 as whitespace. The
tokenizer separates on exactly five — TAB, LF, FF, CR, SPACE — and everything
else in that range is an ordinary character: a NUL inside an attribute name
becomes U+FFFD and stays part of the name.
So `<svg><font color\0=x>` carries an attribute the parser writes as
`color�`, and the `<font>` stays foreign. The walker recorded a plain
`color`, `isForeignBreakoutTag` broke out to HTML early, and the CDATA's
authored `</body>` became the chosen boundary: offset 89 against parse5's 129,
bridge dead.
It is not only NUL. Codes 1, 8, 11, 27 and 31 all invented the same attribute.
All six `<= 32` tests — five in `scanTag`, one walking the prologue in
`prependAfterDoctype` — now go through one `isHtmlWhitespace` predicate, which
`isEndTagBoundary` also reuses so there is a single definition. Attribute names
additionally replace NUL with U+FFFD, as the tokenizer does, so a name that
carries one is not the name it looks like without it.
Validation: `control-char-attr.html` is red on 77c101e3 and green here (71 route
specs, 84 with the containment suite). Every C0 code point was checked against
parse5's attribute list in six tag positions — 165 samples, 0 mismatches, where
before the fix each of the tested control characters produced a wrong name.
`scanTag`'s 57,902-tag differential is still 0 mismatches, `pnpm guard` and
daemon typecheck exit 0, and the corpora are unchanged at 0 broken.
* test(daemon): pin the script-data-double-escaped exit through `-->`
Review asked for two things here: change the `-->` transition, and cover this
state in the route matrix. I am not making the change — measured against parse5,
the current form matches on 35,367 terminated scripts (14,113 of them carrying a
double-escaped sequence) with 0 mismatches, while the proposed form mismatches
1,942, every one of them closing late or not at all. The reply on the thread
carries that evidence and the spec citation, and I left it open rather than
resolving it.
The coverage is worth having regardless of who is right, so this adds it.
The document from review has a second `</script>` to fall back on, so both
readings eventually find some boundary and it does not discriminate — I wrote
the spec around it first, watched it pass against the proposed form too, and
had to fix it. It is kept as the first half because it is the shape that was
reported. The second half is the same state without the fallback: read as
staying double-escaped, that script never closes, swallows `<main>` and the real
body close, and the scan is left with no boundary at all.
So the spec asserts live elements — a bridge inside `<body>` and `#slot` still
an element rather than script text. It is green on this head and red with the
proposed transition, which is the property a spec about a disputed transition
needs to have.
Validation: 72 route specs, 85 with the containment suite. No source change;
`pnpm guard` and daemon typecheck exit 0.
* fix(daemon): three tree-construction transitions the scan was reading wrong
All three from review, all reproduced against parse5 before changing anything.
A solidus only self-closes when `>` comes immediately after it — the
self-closing start tag state reconsumes anything else in the
before-attribute-name state. `scanTag` kept the flag set across whitespace, so
`<svg/ >` looked closed and its contents were read as document markup: the
`</body>` inside the CDATA became the boundary, offset 76 against parse5's 109,
bridge dead.
The dispatcher hands a `<svg>` start tag beneath a MathML `annotation-xml` to
the HTML rules even when `encoding` is absent, so that element lands in SVG
rather than inheriting MathML. Inheriting it meant a `<foreignObject>` under it
was not an integration point and its `<script>` was scanned as foreign markup:
116 against 197, bridge spliced into the script string.
`prependAfterDoctype` walked only `<!-- … -->` before the doctype, but every
token the tokenizer turns into a comment may precede one without changing the
document's mode. `<?xml version='1.0'?><!doctype html>…` put the payload in
front of the prologue, which puts a character token before the doctype and
drops the document from no-quirks to quirks — a silent change to the artifact's
box model. Review reported the XML prologue; measuring showed `<!bogus>` and
`</1>` do the same, so the walker now consumes bogus comments generally, via a
predicate that deliberately excludes `<!doctype` itself.
Validation: `solidus-then-space.html`, `annotation-xml-svg.html` and
`bogus-prologue.html` are red on 126e6431 and green here (75 route specs, 88
with the containment suite). Document mode is checked through parse5's own
`mode` on eight prologue shapes — before and after injection agree on all of
them, and a leading BOM still stays at byte zero. `pnpm guard` and daemon
typecheck exit 0. Every standing differential is unchanged: 57,902 tags, 165
control-character samples, 2,433 repository files, 3,000 adversarial documents,
tag-soup on three seeds, and 6,000 mixed-hazard documents — 0 broken.
* fix(daemon): the in-select insertion mode ignores a foreign start tag
From review, reproduced first: `<select><svg></select><script>const x =
"<table></body>";</script>` selected offset 86 against parse5's 163, spliced
the bridge into the string literal, and reparsed with no live bridge. parse5
confirms there is no `svg` element in that document at all.
The cause is one step earlier than the scan: the in-select insertion mode
ignores an `<svg>` / `<math>` start tag outright, so no foreign element is
created and the bytes after it are still ordinary HTML. Walking them as foreign
content made the `<table>` inside the script string look like a breakout and
handed back that string's `</body>`.
The top-level scan now counts open `<select>` elements and does not enter
foreign content while inside one. That is the only HTML insertion mode this
scan knows about, and the comment says so.
Review offered a second option — refuse whenever a non-root end tag matches no
foreign stack frame — and I measured it rather than choosing on taste. It
returns -1 on 5,596 of 13,880 generated foreign subtrees and raises mixed-hazard
degradations from 792 to 2,131, because most unmatched end tags are not the
dangerous case: walked down to the first HTML ancestor and reprocessed, they
meet a `special` element and are ignored, which is why parse5 keeps parsing
through them. The narrower fix costs nothing: the foreign-subtree differential
stays at 0 mismatches on 13,880 explicit closes and 10,591 breakout closes, and
degradations stay at 792.
Validation: `select-foreign-start.html` is red on ab21860c and green here (76
route specs, 89 with the containment suite). `pnpm guard` and daemon typecheck
exit 0. Corpora unchanged — 2,433 repository files, 3,000 adversarial documents,
6,000 mixed-hazard documents, 0 broken.
* fix(daemon): let the foreign stack be the subtree's only lifetime
Two findings from review, both regressions from my own recent commits, both
reproduced before changing anything.
`skipForeignContent` tracked the subtree with a name counter beside the stack.
A nested element can share the root's name without being another copy of it:
in `<svg><foreignObject><svg></foreignObject></svg>` the inner `<svg>` is an
ordinary element, and when `</foreignObject>` truncates the stack past it the
counter is left holding a frame that no longer exists. The walk then never
returned, the following `<script>` was read as SVG markup rather than HTML raw
text, and the `<table>` inside its string looked like a breakout — offset 113
against parse5's 158, bridge dead. `<math><mi><math></mi></math>` is the same
shape.
The counter is gone. The stack is the lifetime: the unwind loop now includes
frame 0, so a matching root end tag pops the root, and an empty stack — from an
end tag or a breakout pop — is what ends the walk.
The `<select>` guard added in d2b18b7a counted nesting, but a `<select>` start
tag while a select is open *closes* it rather than nesting. The counter reached
2 and the guard stayed asserted after the select had actually ended, so
`<select><select></select><svg><![CDATA[x > </body>]]></svg>` suppressed the
foreign walk and took the boundary from inside the CDATA. It is a flag now, and
a second start tag clears it exactly as the end tag does.
Validation: `nested-svg-foreignobject.html`, `nested-math-mi.html` and
`nested-select.html` are red on d2b18b7a and green here (78 route specs, 91 with
the containment suite). A genuinely nested `<svg><svg>` still resolves
correctly, as do the breakout, CDATA and select cases from earlier rounds.
`pnpm guard` and daemon typecheck exit 0. Every differential is unchanged: the
foreign-subtree comparison stays at 0 mismatches across 13,880 explicit closes
and 10,591 breakout closes, and 2,433 repository files, 3,000 adversarial
documents, 6,000 mixed-hazard documents and tag-soup on three seeds are all 0
broken.
* fix(daemon): model every way the in-select mode ends
From review: `input`, `keygen` and `textarea` start tags close an open
`<select>` and are then reprocessed, so the SVG after them really is foreign
content. The flag stayed set, the foreign walk was skipped, and the CDATA
declaration was ended at its first `>` — handing back the authored `</body>`
inside it. Reproduced at offset 73 against parse5's 116.
Rather than adding the three tags named, I enumerated the mode's exits against
parse5 by asking, for each candidate token, whether an `svg` element exists in
the resulting tree. That gives the whole set rather than the reported part:
* `</select>`, and a second `<select>` start tag, in every context.
* `input`, `keygen`, `textarea`, in every context.
* `caption`, `table`, `tbody`, `tfoot`, `thead`, `tr`, `td`, `th` — but only
inside a table, because outside one the in-select mode ignores them and the
select stays open. So the table is tracked rather than assumed; treating
those as unconditional exits would clear the flag on documents where the
browser has not left the mode, which is the same defect pointing the other
way.
* `option`, `optgroup`, `p`, `div`, `b`, `hr` do *not* exit — confirmed, not
assumed.
`textarea` also needed the select state updated before the raw-text branch,
which was consuming the tag first.
I did consider refusing a boundary outright while in a select, which needs no
modelling. It is not affordable: 904 of 2,433 HTML files in this repository
contain a `<select>` — 37% — while only 4 contain both a select and an
`<svg>`/`<math>`. The interaction is rare; the element is not.
Validation: `select-exit-input.html`, `select-exit-keygen.html` and
`select-exit-textarea.html` are red on f73b813a and green here (79 route specs,
92 with the containment suite). The three exits resolve to parse5's offset
exactly; the six non-exits keep a live bridge. `pnpm guard` and daemon typecheck
exit 0, and every corpus is unchanged at 0 broken.
* fix(daemon): decide the select's table context before consuming the token
From review: `tableDepth` was incremented before the select-exit test, so
`<select><table>` at depth zero made itself look like "in select in table" and
ended the mode on its own token. The browser ignores that table outright and
stays in select, so the `<svg>` after it is ignored too — parse5 confirms
neither element exists. Reproduced at 84 against parse5's 138, bridge dead.
Both decisions are now made against the state before the current token, and a
table token the in-select mode ignored never joins the table stack.
Then I fuzzed the guard instead of hand-writing cases for it, which is what I
should have done when I first added it. 20,000 documents assembled from select,
table, exit-token and foreign-content fragments turned up nine failures of a
shape I had not imagined: `<table><keygen><select></tr><svg>…`. parse5's tree
shows why — foster parenting moves the `<select>` out of the table, so the mode
is plain "in select" and `</tr>` is ignored. Whether those tokens end the mode
depends on the select being in *table scope*, a property of the open-element
stack rather than of having seen a `<table>` start tag, and a linear scan cannot
tell the two apart.
Both answers are wrong in some document — skipping the foreign walk when the
mode has ended, taking it when it has not — so that case now refuses and the
caller falls back to an anchor it can prove. Cost, measured: nine of 20,000
generated documents move from corruption to degradation, and nothing else
changes.
Validation: `select-table-token.html` is red on 189bb379 and green here (80
route specs, 93 with the containment suite). The select/svg, select+input and
in-table select cases from earlier rounds all still resolve as before. `pnpm
guard` and daemon typecheck exit 0; the select fuzz is 0 broken, and the
standing corpora are unchanged at 0 broken.
* fix(daemon): respect an authored base inside an HTML integration point
From review. The containment base is suppressed when the document already has
one, and that check asked the structural scanner — which skips whole `<svg>` /
`<math>` subtrees, because for its own purpose nothing inside one can be a
document boundary. A `<base>` under `<foreignObject>`, `<desc>` or a qualifying
`annotation-xml` is not a boundary but it is an HTML-namespace element, and it
governs the document.
Confirmed in Blink rather than argued from the spec. Parsing the reported
document gives `baseURI = https://author.example/assets/`; adding the generated
base to `<head>` moves it to the generated URL, because the first base with an
href wins. So the injection silently repointed every authored relative URL.
The same check confirms the two shapes that must *not* count: a `<base>`
directly under `<svg>` is an SVG-namespace element and leaves `baseURI` at the
document URL, and one inside a `<template>` belongs to an inert fragment. Both
are indistinguishable from the real thing to a linear scan, which is why this
uses the parser already imported here rather than extending the scanner.
The parse is gated on the document containing foreign content at all, since
nothing else can hide a base from the scan: 252 of 2,433 HTML files in this
repository reach it — 10% — and the largest of those parse in 3.6–7.7 ms.
Validation: `integration-point-base.html` is red on 8d2ba161 and green here;
`foreign-namespace-base.html` is green on both, which is the point — it pins
that the fix does not over-reach. 82 route specs, 95 with the containment
suite. `pnpm guard` and daemon typecheck exit 0.
* fix(daemon): share the in-select mode with the template walker, and namespace the base's template test
Two findings from review, both reproduced first, both in code from the last few
commits.
`skipTemplateContent` entered foreign content unconditionally while the
top-level walker modelled the in-select mode. A template's contents are parsed
with the same insertion modes as the document, so the two disagreed on the same
bytes: `<template><select><svg></select><script>const x = "<p></template>…"` had
the nested walker break out on the `<p>` inside the string and then take that
string's `</template>` as the template close — 113 against parse5's 169, bridge
dead.
Rather than copying the logic across, the mode is now one `SelectModeState` that
both walkers fold tags into. Duplicating it is what produced the divergence, and
a second copy would produce the next one.
The base check's template test was by name only. A foreign element may also be
called `template` and it creates no inert fragment, so in
`<svg><template><foreignObject><base href="…">` the base is an HTML-namespace
element and live — Chromium resolves the document against it — while a name-only
ancestor test discarded it and let the generated containment base be inserted
ahead of it. The test now requires the ancestor to be both named `template` and
in the HTML namespace. That is the same "name without namespace" mistake this
file exists to avoid, in code I had added an hour earlier.
Validation: `template-select-foreign.html` and `svg-template-base.html` are red
on 0467e101 and green here (84 route specs, 97 with the containment suite). The
top-level select cases, template foreign content, and the five base shapes from
the previous round all still resolve as before. `pnpm guard` and daemon
typecheck exit 0; every corpus is unchanged — 2,433 repository files, 3,000
adversarial documents, 6,000 mixed-hazard documents, 20,000 select/table
documents, foreign subtrees and tag soup, all 0 broken.
* fix(daemon): the two stray end tags reprocessing acts on, and per-template select state
Two findings from review, both reproduced first.
An unmatched end tag inside foreign content is walked down to the first
HTML-namespace ancestor and reprocessed in the current insertion mode. My
comment claimed it always meets a `special` element there and is ignored. That
is true for most of them and false for two, so I measured instead of reasoning:
of 35 stray end tags served through a real parser, 33 leave the `<svg>` open and
`</p>` and `</br>` close it — both have an "in body" rule that synthesises the
element and then closes it, popping whatever is open above.
Those two now refuse. The other 33 keep their precise boundary, which matters:
the blanket refusal I measured in an earlier round cost 5,596 of 13,880 foreign
subtrees, and this costs none — the corpora's degradation counts are unchanged.
`skipTemplateContent` carried one select state across every template depth. A
nested template's contents are processed through the template rules, which start
a fresh insertion mode and restore the outer one at its close, so an `<svg>`
inside the inner fragment is real foreign content even when the outer fragment
is in select mode. Carrying `inSelect` through made the walker ignore it and
then take the `</body>` out of a CDATA section — 118 against parse5's 192.
There is now one state per open template, and that stack is the depth, so the
count and the modes cannot disagree.
Validation: `foreign-stray-p.html` and `nested-template-select.html` are red on
c017e4cc and green here. `foreign-stray-div.html` is green on both, which is its
purpose — it pins that the refusal did not widen to the 33. 87 route specs, 100
with the containment suite. `pnpm guard` and daemon typecheck exit 0; every
corpus unchanged at 0 broken, including the degradation counts.
The od:url-selection-bridge-ready latch now requires the message to carry the document href and match the current iframe src, so a stale ready from a previously loaded document can no longer keep Comment on a bridge-less URL-load preview. The daemon bridge posts href in both the probe reply and the initial ready. Falls back to srcDoc comments when no matching ready arrives.
Fixes#6702
* fix(web): scope plugin URL imports to workspace
* test(web): cover billing interest remount lifecycle
* feat(packaged): enable the workspace-team transport for test-profile builds
The vela test backend now serves the workspace-team API, so a packaged build
baked with the test AMR profile should drive team projects / collab / resource
sharing there instead of leaving them dormant. Replace the single feature-test
equality check with a profile -> vela web origin map so adding a backend is one
entry, and keep every unlisted profile (prod above all) off.
* test(daemon): cover billing catch-up after SSE reconnect
* fix(collab): surface and start first materialization when opening a shared project
QA P0: a brand-new member on a fresh install joins someone's workspace, opens a
project from it, and sees no loading state and no files — content only appears
on a SECOND open, much later.
Two defects, both on the first `/collab/status` request that opening a shared
project makes.
1. No pull is started. The daemon's self-materialization block was gated on
`callerIsOwner`, so a non-owner member got the placeholder record registered
and nothing else. The web's auto-pull is gated on `publishedVersion`
advancing past its cursor, and a fresh daemon's first status response cannot
carry a published head: `collab.publishedVersion()` reads an in-process map
that has never been written, and the real hub head is fetched
fire-and-forget into `headEnrichmentCache` for a LATER poll to consume.
Materialization therefore arrived only whenever a proactive lane (hub push /
reconnect catch-up / the 30s recovery floor) next fired — the "second open"
the report describes.
2. No download state is shown. `useProjectCollab.downloadPending` — which picks
DesignFilesPanel's "Syncing files from the team…" over its empty state and
create-a-file CTAs — reads only `publishedVersion > cursor`,
`contentTransferState`, and an in-flight pull. All three are blank on that
first response, so the member was shown an empty project inviting them to
start creating over content that was still on its way.
The fix makes the placeholder stamp the load-bearing signal on both sides:
- `materializePlaceholderOnOpen` (routes/collab-sync.ts) replaces the
owner-only block: any viewer opening a shared project whose only local record
is an unmaterialized placeholder starts its pull on that same request,
through the same coalesced flow. The retracted-share heal in its catch stays
owner-only — a member who hits the hub tombstone has merely lost access and
must not unshare anyone's project.
- `/collab/status` reports `awaitingFirstMaterialization`, a purely local,
synchronous fact that needs no hub round-trip and is therefore available on
the very first response. `od collab status` prints it too.
- `localFilesAreNotTheContentYet` (useProjectCollab.ts) makes it always
download-pending, bypassing the member-only `shouldAutoPull` gate: whether the
local files are the content is a fact about the record, not about who may
pull it, so the owner of an unmaterialized placeholder (the reinstall case,
recvqzaDvUU6B3) stops seeing the same empty state too.
Red evidence: all six new specs fail on origin/feat/workspace-team @ 793d7b92c
— the first status response reported `awaitingFirstMaterialization: undefined`
with publishedVersion/materializedVersion/contentTransferState all null,
`beginContentTransfer` was never called for a member's first open, and
`downloadPending` was false for both the member and the owner.
* chore(packaged): inject the vela web origin at build time, not source
This repository is public. Commit 319cb42b3 added a profile -> vela web
origin table to apps/packaged/src/sidecars.ts, which put an internal
environment's hostname into product source and would have published it on
merge. The same hostname had also reached apps/web/src/runtime/amr-guidance.ts
as a new profile row.
Internal AMR environments are not public (one of them fronts a Stripe
sandbox), so their origins now travel the same build-time path POSTHOG_KEY
already uses:
release-beta.yml env (per-profile secret)
-> tools-pack config (OD_VELA_WEB_URL)
-> open-design-config.json (velaWebUrl)
-> packaged config -> daemon spawn env (OD_VELA_WEB_URL)
-> GET /api/integrations/vela/status (consoleOrigin)
-> web runtime console links
The workspace-team gate keeps its meaning and gets stricter: it now needs an
allowlisted AMR profile AND an injected origin. `prod` can never satisfy the
profile half, so a stable build stays dormant however it is configured, and a
feature-test/test build whose secret is unset stays dormant too rather than
pointing the transports at an unknown backend.
A build with no secret configured therefore ships with workspace-team silently
off — the same failure mode as a fork build without POSTHOG_KEY, and the safe
direction for an unreleased feature.
apps/packaged/tests/source-origins.test.ts is a standing guard: every absolute
URL literal under apps/packaged/src must resolve to a publishable host, so the
next backend origin cannot be hardcoded there either.
Secrets required for a workspace-team dogfood build:
VELA_WEB_URL_FEATURE_TEST, VELA_WEB_URL_TEST.
* test(e2e): follow the #5517 plugin-details route in the visual capture
`captures the home plugin use staged surface` has been failing on this
branch since before the main merge. It was written against the pre-#5517
UI: clicking a marketplace card opened a role="dialog" details overlay
with a per-slug `plugin-details-use-<id>` action.
#5517 made plugin details a full-page route — `openCardDetail` calls
navigate({ kind: 'marketplace-detail' }) for plugin records, rendering
`PluginDetailView` at /marketplace/<id> whose Use control is a single
`plugin-detail-use` button. Per the product rule that the accepted
feature-branch UI is the source of truth, the test follows the route
instead of the UI being changed back.
Asserting the route alone was not enough: the detail surface refetches
the record from /api/plugins/<id>, which the visual fixture never mocked
(it mocks the list, */preview and */apply), so the route rendered its
"Failed to load plugin: HTTP 404" branch. configureVisualPage now serves
the single-plugin GET from the same VISUAL_PLUGINS fixture.
The capture asserts the Use button rather than the `plugin-detail` shell
because the shell also renders for the loading and load-failed states.
* fix(packaged): restore masked CLI icons (#6154)
* Sync local workspace-team batch: UI polish, popover simplification, update-reminder progress (#6156)
* UI polish batch: import-dialog cleanup, settings toast placement, tab chrome fixes
- Plugin/skill import dialog: drop per-card icons, left-align card content
- Settings page: center the autosave pill under the top nav
- Avatar model menu: soften the pinned footer divider
- Workspace tabs: pinned entry tab always reads as Home; remove tab-search
button and its popover
- Model picker: wider row gap in the two-pane browse popover
- Community header: unpin (scrolls with content)
- New update-reminder dialog assets and strings
- Keep entry-settings-button as the signed-out settings entry (e2e contract);
align NextStepActions with the navPlugins key rename
* Update reminder: simulate in-place download progress instead of linking out
The strip's confirm now runs a staged progress state ('updating') with an
eased fake download bar, then marks the version updated — replacing the
jump to the GitHub releases page.
* test(web): retire AvatarMenu account-row suites after the popover simplification
The composer popover was reduced to a model picker (2026-07-24): the Open
Design account row — plan badge, balance, wallet fallback, upgrade/console
links — no longer renders there, so the nine suites asserting that surface
(including the workspace-balance and billing-permission gates that landed
upstream meanwhile) can no longer pass. Drop them and recast the remaining
account-row test as the guard for the new invariant: a signed-in AMR status
must render no account UI in the popover.
Also drop the stale onClose prop from the new upstream sign-out-confirm
test; the rail no longer takes one.
* Design-systems toast: anchor to the pane's top edge, pill radius
* Update reminder strip: progress ring on the button, percent face
While updating, the collapsed strip no longer stacks a label + bar above
the rocket button. Progress draws as a brand-green ring hugging the round
button's edge (5px band reserved so the button never shifts), and the
button face swaps the rocket for the bare percent — 'N%' while
downloading, 完成 (updater.done) at 100. The full 正在下载更新 N% label
stays reachable via the hover bubble and the ring's aria-label. Demo
payload version bumps to 1.4.6 so the once-per-version card re-arms.
* fix(web): restore design kit section spacing (#6155)
* fix(web): carry the plugin back to Home when Use runs from the detail route
Product call: using a plugin from the full-page detail route should land on
Home with that plugin already selected and its brief pre-filled, exactly
like the marketplace card's "Try it".
It did not. `PluginDetailView.onUse` applied the plugin, kept the result in
local state, and navigated home. `App` renders that route outside
`EntryShell`, so the navigation unmounted the holder and dropped both the
plugin and the brief — Home came up empty. The "redirected to Home with the
brief pre-filled" note under the button could never be seen either, since
the navigation fired immediately; it is removed rather than left lying.
Reuses the existing channel instead of adding a parallel one: onUse now
publishes the same `createPluginUseHandoff` payload "Try it" produces, and
Home's existing pendingPluginUseHandoff path applies it unchanged. The
handoff is parked in module scope — not on `window` — so it survives the
unmount, and `EntryShell` claims it in a lazy state initializer so it
arrives in the same commit as the mount. Reads are destructive, so a
handoff applies once and does not re-fire on the next visit to Home.
Red first: PluginDetailView.use-handoff.test.tsx fails on the unfixed code
at the handoff assertion (navigation and the failure path already passed),
and goes green with this change. The e2e visual capture that asserts
home-hero-active-plugin should follow without weakening its assertion.
test(e2e): match the settings surface in both #5517 presentations
openSettingsDetailsFromHeader still waited on `.modal-settings[role="dialog"]`.
#5517 routes the entry's settings to /settings, where SettingsDialog renders
in presentation="page" mode with `role="region"`, so that selector never
matched the page presentation: an already-open settings surface looked
absent and the helper fell through to hunting for entry triggers that
surface does not carry, failing 16 captures on one line.
Reuses `settingsSurface` from amr.ts — the existing bare `.modal-settings`
matcher both presentations share — rather than adding a third local
variant. That helper also documents why the `role="dialog"` fallback was
actively wrong: AvatarMenu is a dialog too, so it could resolve to the
account menu.
* test(e2e): open the nav rail before reaching for entry settings
The bare-`.modal-settings` change alone did not move the settings-workspace
lane: all 16 captures still died on `.settings-icon-btn`, because the
failure happens before the surface selector matters. None of the four
triggers was ever visible.
amr.ts already documents why. #5517 moved the entry settings chip into the
nav rail footer, and a collapsed rail is `inert` + `aria-hidden`: the chip
is present but invisible to `getByRole`, and even a programmatic
`element.click()` is a no-op. `openSettingsDialog` expands the rail first
(`ensureEntryRailOpenIfPresent`), clears loading, dismisses the privacy
dialog, matches the surface with the shared bare class, and ends its
trigger chain on the settings aria-label instead of `.settings-icon-btn`,
which the entry surface does not carry. It also clicks
`entry-settings-open-details`, so it is a superset of the local copy.
So `openSettingsDetailsFromHeader` now delegates to it and keeps only its
name, leaving exactly one settings opener instead of a third variant.
* test: realign the suites #6156 outpaced, and reach Settings from a project
#6156 (4e3161751) landed a UI batch whose test fallout was only partly
handled. Four Vitest files and four e2e call sites still describe the surface
as it looked before it, and the Playwright settings opener never had a trigger
that exists once a project is open.
Vitest — the assertions now read the surface that shipped:
- community-view: the type tabs traded their `<small>` count badges for type
icons, so `readFacets().badge` was `NaN`. Counts move to a new
`readFacetCardCounts()` that drives each tab and counts the grid — the same
array the badges were derived from, so the catalogue-fidelity guard
(including "never grids more than the whole catalogue") survives the
badge's removal rather than being dropped.
- home-logo-assets: the rail's signed-out brand header is gone (with no cloud
identity the rail starts at the search box), so it can no longer carry
`od-brand-glyph`. Keeps the guard that still means something: it must never
fall back to the retired raster app icon.
- acceptance-visual-fixes: recvq4iEq1Esno's fix is intact — page mode still
hides only `.settings-chrome-btn`, leaving the autosave pill visible. #6156
re-added the bare `.settings-page-shell .settings-chrome` selector purely to
centre that pill under the top nav, so assert the invariant the row is
about (no page-mode `display: none`) instead of the selector's absence.
- FileViewer: the new `artifact-preview-first-load` cover is a `role="status"`
of its own, so `findByRole('status')` had become ambiguous. Scope the
success assertion to the toast.
e2e — `entry-nav-settings` no longer exists anywhere in `apps/web/src`; the
rail item carries `entry-settings-button`. #6156 renamed it and updated
entry-chrome-flows' two `toHaveCount(0)` assertions but not the four places
that click or await it (critical-smoke, entry-topbar x2, entry-chrome-flows x2).
`openSettingsDialog` also only ever knew entry-shell triggers, all of which
live on Home. From a project every one of them is absent — and
`EntrySettingsMenu` and `AppChromeHeader`'s `SettingsIconButton` are both
unrendered — so the surface never opened and the failure surfaced as a missing
`.modal-settings`. Add the project surface's real entry: the composer model
popover's pinned `avatar-open-execution-settings`, with the topbar switcher's
`inline-model-switcher-open-settings` as a second route.
Not addressed here, deliberately: #6156 also deleted the `settings-general-field`
block that rendered `AppearanceSection`, orphaning the component. That removes
the product's only System/Light/Dark control (the rail's account menu dropped its
theme row *because* Settings owned it), so the 11 `SettingsDialog.execution`
appearance failures are reporting a capability loss, not a stale expectation.
Restoring it is a product call, so those tests are left red rather than weakened.
* test(e2e): finish the popover the settings chain opens on a project surface
`openSettingsDialog`'s trigger chain ends on OPEN_SETTINGS_LABEL, which matches
`Account & settings` — and that is `avatar.title`, the label on AvatarMenu's
own trigger. So on a project surface the chain does find something and clicks
it, but what opens is the composer's model popover, not Settings. The
follow-through step then looked only for `entry-settings-open-details`, which
#5517 left unrendered, so the run gave up with a missing `.modal-settings`
after three identical attempts.
Widen the follow-through to the two rows that actually route to Settings from
a project — AvatarMenu's pinned `avatar-open-execution-settings` and the topbar
switcher's `inline-model-switcher-open-settings` — so whichever popover the
first click opened gets finished.
* test(e2e): pin the AMR selection assertion to the agent card
With Settings actually opening from a project surface again, the surface-wide
`/Open Design/i` in amr-logout-requires-relogin resolves to the sidebar's
"Open Design MCP" `settings-nav-item` — which carries no `aria-pressed` — not
to the AMR agent card's select button. The looseness was invisible while the
surface never opened.
Read the toggle through `settings-agent-card-amr` instead, so the assertion
names the thing the test is about: AMR stays the selected agent.
* test(e2e): prove Settings opened via its section nav, not a heading probe
`prepareVisualSettingsDialog` gated on a heading matching
/Settings|General|Execution mode/. Now that the surface opens from a project it
lands on the execution section, whose heading reads "Models & providers" — and
the surface's own <h2> is consumed as its accessible name via aria-labelledby
anyway, so the probe could not match in either presentation.
Assert `settings-nav-execution` instead, which is what critical-smoke already
uses for the same purpose and holds for both the modal and the routed page.
* test(web): retire the theme/appearance cases the product cut
Product decision (2026-07-28), verbatim: 「主题设置不要了,因为 workspace 功能不
支持暗色主题,要干掉,并且之前用户如果设置了暗色主体,需要强制改成明亮色主题」
So #6156 deleting the `AppearanceSection` render site was the right direction,
and the NON-ALIGNMENT #9 note that argued for keeping the segmented control as
the product's last "follow system" entry point is formally overturned. These
cases are retired because the capability is gone by decision — NOT to turn CI
green. The source-side removal (orphaned `AppearanceSection`, onboarding
light/dark toggle, `settings.appearance*` keys, `trackSettingsAppearanceClick`,
plus forcing stored dark/system back to light for existing users) lands
separately on `fix/remove-theme-force-light`.
Deleted (8) — their whole subject was theme or the accent it carried:
- offers the theme segmented control, and System leaves the document theme unset
- applies the stored default accent color even though the picker is gone
- writes the picked theme to the document and autosaves it
- live previews the configured theme on open, and System leaves no explicit
document theme
- reverts an unsaved appearance preview back to the saved appearance when the
dialog closes
- persists System mode explicitly and preserves accent variables without an
explicit document theme
- keeps a stored non-default accent applied and carries it through an autosave
- localizes the theme controls in Chinese
Nothing was left behind as a loose "still renders something" assertion, and no
live coverage went with them: the close-button/`onClose` path the revert case
also touched is already covered at five other call sites in this file.
Kept (3) — theme was only the vehicle, the subject is still shipping. Each now
rides the notifications completion-sound toggle, seeded on so the single click
is a real state change (the pills no-op when clicked in their current state):
- reconciles the open settings draft when the parent agent CLI env changes —
the only integration proof that `reconcileAmrModelChoice` /
`reconcileAmrProfileEnv` actually run through the real draft on autosave;
its two siblings are pure unit tests and do not cover that path.
- drops a pending autosave when explicit onboarding reset unmounts Settings —
subject is the pending-autosave drop and the draft the reset carries.
- still autosaves an unrelated edit that lands during a silent-update save —
subject is autosave bookkeeping (success must only advance
autosaveLastSavedRef for allowSilentUpdates).
The enclosing block is renamed 'SettingsDialog draft reconciliation' since it no
longer describes appearance, and its document theme/accent teardown is dropped —
nothing in it writes those any more. 144/144 green.
* test(e2e): realign the visual workspace captures with the narrowed surfaces
Three deliberate narrowings shipped without this Playwright lane following, so
eight captures were driving UI that no longer renders. All eight are stale
fixtures; none was a regression.
- `ef9c8cd8b` made the home top-bar `InlineModelSwitcher` `compact`, and
`EntryShell` (its only call site) always passes the flag. In compact mode the
popover is just the active agent's model radio list plus the route to
設定 → 執行; the mode segmented control, agent grid, account block, and both
searchable dropdowns live in the non-compact branches. `68cecac1c` records
that product confirmed this narrower shape, so acceptance #40 ("home 页不能
切换 cli 了") is working as designed.
- `4e3161751` (#6156) removed the Open Design account row from `AvatarMenu`
(plan badge, balance, wallet fallback, upgrade/console links) and retired the
nine equivalent Vitest suites — but not these.
- `56b538aa4` moved the design-system picker out of the staged-context bar, so
`staged-contexts` is no longer unconditionally mounted.
Where a surface still ships, the capture follows it and the assertion got
sharper rather than looser: staged contexts now stages a real attachment and
names the chip; the topbar/avatar captures assert the surviving model picker
plus the *inverse* invariant (no account block, no upgrade button, no balance),
gated on a counted `vela/status` request so a negative cannot pass by racing the
fetch. `:57` additionally pins `inline-model-switcher-mode-daemon` at count 0, so
re-mounting the CLI console on Home fails here again.
`:167` (topbar BYOK model dropdown) is deleted: in compact mode it would
re-capture the identical popover as `:141`, which is a duplicate screenshot
rather than coverage. `:141` absorbed its guard.
`:244` (avatar reasoning readout) turned out never to have passed — not a #6156
casualty. `VISUAL_CLI_AGENTS`' codex entry declares `models` only, while
`AvatarMenu` draws the readout solely for agents reporting `reasoningOptions`, so
it has been red since `68cecac1c` authored it. Fixed by declaring
`reasoningOptions` on a test-local codex fixture, matching what the real daemon
reports (`apps/daemon/src/runtimes/defs/codex.ts:164`).
Nothing was dropped without a verified live home elsewhere in the same CI lane
or in Vitest: the AMR plan/balance/upgrade card at `visual-settings.test.ts:30`,
the searchable dropdowns at `visual-settings.test.ts:93`/`:156`, the upgrade
attribution URLs in `InlineModelSwitcher.test.tsx:585` and
`AvatarMenu.test.tsx:283`, the "no account row" invariant at
`AvatarMenu.test.tsx:528`, and real staged-context chips in
`project-management-flows.test.ts`. The one assertion with no surviving home is
the Open-Design-first agent ordering, whose grid no longer renders anywhere;
that is called out in the test comment rather than silently lost.
* test(e2e): give the AMR upgrade capture a billing-capable workspace
`settings-agent-card-amr-upgrade` stopped rendering for this capture because
`amrCardCanUpgrade` (SettingsDialog.tsx:4651) now also requires
`workspaceContext?.permissions?.canManageBilling` — this branch's
workspace-scoped billing work, i.e. the feature #6142 exists to ship. The gate
is correct; the fixture was not representing anyone entitled to pass it, since
`mockSignedInVelaAccount` establishes a Vela session but no workspace context.
Stub `/api/workspace/context` with a personal-owner context inline in this one
test rather than widening `mockSignedInVelaAccount`, which visual-workspace
shares. The assertion is unchanged — the upgrade entry is a real capability for
billing-capable members, so the fixture represents one instead of the assertion
being relaxed.
Verified the stub does not quietly satisfy the three assertions that already
passed: `planId: null` keeps `resolvePlanTier` falling through to `plus`, and
billing still 404s so the balance stays `$247.51`.
Expect one legitimate baseline diff on `visual-settings-open-design-account`:
the rail now renders signed-in workspace chrome, which is the only state in
which the upgrade entry exists at all.
* test(e2e): leave projects through shipping chrome, and reach /projects reliably
Two dead locators and one under-budgeted wait, all test-side.
`openNewProjectModal` (rail.ts) probed `entry-nav-new-project` then
`entry-nav-projects`, both deleted from EntryNavRail by the #5517 redesign
(`b55f17169`, `f16075f7e`) — `onNewProject` is still destructured there but has
zero call sites. Its body is byte-identical to main, where both testids exist, so
main takes the rail path and never reaches the fallback this branch always hits.
The live affordance is `designs-new-project` / `designs-empty-new-project` in
DesignsTab, inside `entry-view-projects`; `/projects` has no UI entry at all
(entry-chrome-flows:373-382 already documents that and routes directly).
The fallback then failed for a second reason: `entry-view-projects` does ship
(EntryShell.tsx:1579) and `/projects` does route to it, but it was asserted on
the 10s default budget right after `waitUntil: 'domcontentloaded'`. `apps/web`
mounts `src/App` via `dynamic(..., { ssr: false })`, so domcontentloaded fires
while only the boot shell exists — every suite that does this correctly waits out
`Loading Open Design…` with `T.long` first. Now routed via
`history.pushState` + `popstate` (the client router listens) instead of a full
reload, boot shell waited out, and both the view and the create button given
`T.long`.
`ensureRailOpen` was also called unconditionally, but it ends in a hard
`expect(workspace-home-rail-toggle).toBeVisible()` and that testid only renders
for `isPinned && active` (WorkspaceTabsBar.tsx:1474-1510) — from inside a project
it does not exist, so the helper hard-failed on a control this flow never needed.
Now best-effort and gated on `.entry`, kept only so the
`visual-new-project-modal` baseline does not churn.
`real-daemon-run.test.ts:547` hung on `/back to projects/i`, which matches
nothing on a project surface: `AppChromeHeader` owns that aria-label but is no
longer mounted anywhere, and ChatPane's top-left slot resolves
`onCollapse ?? onBack` while ProjectView passes both, so it is always
`chat-collapse-toggle` (deliberate, `884ed1085`). Replaced with a local
`leaveProjectForEntry` that clicks the pinned entry tab — the shipping way out —
so the isolation journey still leaves through real chrome instead of a URL jump.
Dropped `getActionablePoint` with its only consumer. Removing the reload from
~25 call sites across 15 suites was audited against the stub-order-sensitive ones
(settings-media-providers, new-project-ds-picker-*, api-empty-response,
visual-entry); all install routes before their first navigation.
Correcting my own earlier misattribution: `.ws-tab` / `.ws-tab-label` are
FileWorkspace's in-project file strip and are fully intact, `role="tab"` and
file-name labels included. #6156 rewrote WorkspaceTabsBar, which renders
`.workspace-tab` / `.workspace-tab__label` — a different component. Upload→tab is
also intact (`FileWorkspace.uploadFiles` still calls `openFile`). Five of those
six failures merely routed through the broken helper above; the sixth was the
dead back-button. No `.ws-tab` assertion was rewritten.
* fix manual edit across workspace pages (#6160)
* fix(collab): keep process-local presence alive when the collab transport is off (#6170)
`createVelaCliCollabClientFromEnv` returns `null` unless the run opted into
the vela-cli collab transport, but `server.ts` wired the presence routes with
an object literal of arrow functions closing over that client. A literal is
unconditionally truthy, so `registerCollabPresenceRoutes` always saw a `cloud`
dependency: its `deps.cloud ?? null` could never resolve to `null`, the
process-local `presence.present()` fallback became dead code, and every
presence request that reached the cloud branch dereferenced `null` and was
answered `502 collab_presence_unavailable`.
`POST /api/projects/:id/presence/leave` carries no shared-project
precondition, so it failed on any project id in any run without the
transport — every stable/prod packaged build (the packaged workspace-team env
is gated to the `feature-test`/`test` AMR profiles) and every plain
`tools-dev` run. `server.ts` is `@ts-nocheck`, so the null dereference was
invisible to `pnpm typecheck`.
Route the dependency through `createCollabPresenceCloudClient`, which lives in
the checked route module and states the invariant: a `cloud` dependency exists
if and only if a transport exists. Removing its guard is now a typecheck
error, so the bug class cannot come back at the construction site even while
`server.ts` stays unchecked.
* fix(web): scope desktop vibrancy to macOS (#6171)
* fix(web): remove the theme setting and force every install back to light (#6168)
Product removed theme selection: the workspace surfaces shipped for team
workspaces have no dark tokens, so dark mode renders a broken app.
Deleting the picker is not sufficient on its own. Every install that ever
opened it still has `theme: 'dark'` — or `'system'`, which resolves dark on a
dark OS — persisted in localStorage, and a stored value does not move when the
default does. So the theme is now coerced on READ, at all three points a
persisted value can reach the document:
- `loadConfig()` funnels `parsed.theme` through `resolveAppTheme()` and marks
the config migrated so the coerced value is written back once.
- `applyAppearanceToDocument()` stamps `data-theme="light"` unconditionally.
The attribute must be PRESENT, not merely non-dark: every dark CSS rule is
gated on `html:not([data-theme])` / `html:not([data-theme="light"])`, and
every JS theme reader (shiki, ConnectorLogo, SketchEditor, TerminalViewer,
connectorBrandColor, MentionNode) falls back to `prefers-color-scheme` only
when the attribute is absent.
- The pre-hydration inline script in `app/layout.tsx` stamps light before React
mounts, outside its try/catch so a throwing storage read still leaves the
attribute set.
Electron's `themeSource` defaults to `system`, which colours everything the web
layer does not own (macOS vibrancy glass, native menus/dialogs) on a dark-mode
Mac — including the splash, before the renderer's appearance IPC lands. It is
now pinned to light before the first window exists.
Removed surfaces:
- `AppearanceSection` in SettingsDialog, orphaned by #6156 (zero call sites),
together with its `settings-general-block--appearance` styles. Its docblock
claimed the control was "deliberately kept … NON-ALIGNMENT #9"; that decision
is superseded, so the comment goes with the code.
- The onboarding welcome page's sun/moon toggle — the last reachable theme
writer — and the `onThemeChange` prop chain behind it
(App → EntryView/ProjectView → EntryShell → OnboardingView).
- The orphaned theme row in EntrySettingsMenu and its styles.
- i18n keys `settings.appearance`, `settings.appearanceHint`,
`settings.themeSystem`, `settings.themeLight`, `settings.themeDark` across
`types.ts` and all 19 locales.
- Analytics `trackSettingsAppearanceClick`, `SettingsAppearanceClickProps`, and
the `settings_popover` `appearance` element.
`'appearance'` survives only as a legacy settings deep-link token that
`normalizeSettingsSection` folds into General, so an old link is not a type
error.
Red-first: tests/state/force-light-theme.test.ts and
tests/components/theme-settings-removed.test.tsx were written against the
unmodified branch and went red (11 failing) before any source change.
* fix(web): make the update surfaces read real data (#6163)
* fix(web): make the update surfaces read real data
#6156 (4e3161751) shipped two update surfaces whose every field was
invented. `EntryShell` held a private `updateReminderStage` machine keyed on
a literal `'1.4.6'` (the app ships 0.16.1), the rocket's confirm ran a 200ms
`setInterval` easing a fake bar toward 100% and then wrote "updated" to
localStorage, and the cover dialog listed three hardcoded English notes past
i18n over a committed 432 KB JPEG. Nothing in that path made a single
network, IPC or daemon call — a user who clicked "立即更新" watched a
progress ring finish and got no update.
Both surfaces now sit on the real machinery that already existed:
The cover dialog becomes the post-update highlights surface and REPLACES the
bottom-right `WhatsNewPopup` card rather than coexisting with it. Cover art,
release headline, bullets and link all come from the hosted highlights
document via `/api/whats-new`; the title states the running version from
`useAppVersion()` (`/api/version`). It keeps that card's show gate verbatim —
once per highlight `id`, only with content, only on the home surface after
the app returns on a new version — and its existing analytics
(`whats_new_popup` surface-view / click). Its footer is close + open-release;
there is no "cancel / update now", because reporting what already shipped is
all this surface does. Bullets come from a new `whatsNewNotesFromBody`, which
splits the document's `body` per line and strips operator list markers.
The rocket keeps its look and becomes the real updater's ready indicator in
the bottom-left rail footer, replacing that indicator's arrow glyph. It is
gated on the updater's own `shouldShowControl` (installer downloaded and
never opened, desktop only) and still drives `openUpdaterInstaller` →
`quitAfterUpdaterInstallerOpen` through the untouched panel, watchdog and
restart-safety preflight. The progress ring and percent face are gone on
purpose: the indicator only exists once the download has finished, so any
percentage it drew would be invented.
Deleted: `lib/update-reminder.ts`, `UpdateReminderDialog.tsx` and its module
CSS, `public/update-reminder-cover.jpg`, the now-dead
`entry-updater-menu__button/__glyph/__progress` rules, and the unused
`accountNotice` rail slot. The three `updateReminder.*` i18n keys and the
orphaned `whatsNew.dismissAria` are replaced by one `whatsNew.updatedTitle`
across all 19 locales.
A source-level guard (`tests/update-surface-real-data.test.ts`) pins the
invariant that outlives the deletion: no quoted `x.y.z` and no `setInterval`
in the update surfaces, and the placeholder module and cover art stay gone.
* fix(web): name the release-notes destination on the what's-new CTA
Product review of the five open calls closed all five; only the CTA copy
changes. The button opens the release notes, so it now says so —
「查看更新说明」 in Chinese (product's wording, verbatim), with the other 18
locales aligned on "view the release notes" instead of inheriting the old
"see what's new" tease. `whatsNew.cta` had exactly one consumer left (the
dialog) once it replaced the bottom-right card, so the key is repurposed in
place rather than forked.
The other four calls were approved as implemented, and two of them overturn
conventions the old comments still asserted, so those comments are synced:
- `lib/whats-new.ts` documented the toast's deliberate Escape exemption
(Escape hid the card WITHOUT spending the highlight). As a focus-trapping
modal that exemption is retired — every close path marks seen — and the
docblock now says which paths and why.
- `contracts/api/whats-new.ts` described `title` as generic copy, `imageUrl`
as an image "beside the copy", `body` as prose, and `linkUrl` as the "See
what's new" link. The dialog reads them as headline / top-inset cover /
one-bullet-per-line / release-notes link, so each field now documents the
role it actually plays. Comment-only; no shape change.
* fix(web): never state the placeholder version on the what's-new dialog
Review catch (nettee, #6163): `useAppVersion()` reads /api/version at runtime,
so it necessarily boots on `APP_VERSION_PLACEHOLDER` and resolves a round-trip
later. This dialog rendered as soon as /api/whats-new resolved and printed the
hook unconditionally — so a highlights fetch that won that race painted
"Open Design 0.0.0 is here" on first frame. An invented version string, on the
one surface this PR exists to make truthful. The new suite could not see it
because it mocked the hook to an already-resolved value.
Fixed by falling back rather than gating the whole render: gating would delay
the dialog behind a second round-trip on slow networks, while the highlights
document already carries the running version in its `version` field — the
daemon stamps it for display (see contracts/api/whats-new.ts), which is what
fed the old bottom-right card's eyebrow. So `statedAppVersion()` prefers the
resolved hook, falls back to the document, and returns null when neither can
name one; on null the dialog waits, because highlights are worth nothing under
a headline that lies. The same derived value feeds the title AND every
`app_version` prop, so analytics cannot ship the placeholder either.
`APP_VERSION_PLACEHOLDER` moves out of ./provider into a new leaf
analytics/app-version.ts alongside an `isResolvedAppVersion()` predicate, so a
surface that must not PRINT a placeholder can test for it without importing the
analytics client. The provider keeps its own use of the constant.
Regression tests, red before this commit (`.red-evidence-version-race.txt`
reproduces nettee's exact string, `expected 'Open Design 0.0.0 is here…' not to
contain '0.0.0'`): the placeholder never paints while /api/version is in
flight, it never reaches the surface-view analytics, the title switches to the
running version once the hook resolves, and a document with no usable version
waits instead of inventing one. The suite's provider mock is now a mutable
holder so both sides of the race are reachable.
Also caught by this PR's own source guard: quoting the literal in a docblock
tripped it, so the comment names the constant instead.
* test(e2e): unmask the AMR runtime lane and repair the visual settings/workspace captures (#6174)
Three fixture defects, each verified by running the lane rather than reading it.
- amr-run-failure-recovery.test.ts was still `mode: 'serial'`. A second
`describe.configure({ timeout })` cannot clear it — configure only
overwrites the keys it is given — so one failure skipped the eight cases
behind it and the lane reported them as "did not run". e2e/AGENTS.md
forbids serial groups outright.
- configureVisualPage's `**/api/**` catch-all (added by #6126, which is on
main) answered `/conversations` with 404. ProjectView mounts ChatPane only
once a conversation resolves, and both listConversations and
createConversation swallow a non-ok response, so the project opened with no
conversation and no error, ChatPane never mounted, and every capture that
enters the workspace died on `chat-composer` not existing. Supplies the
conversation boundary the catch-all closed without replacing.
- The BYOK tab is named "API providers" since #5971 renamed
`settings.modeApiMeta`; `getByRole('tab', { name: 'BYOK' })` matched
nothing and three captures hung until the test timed out. The topbar BYOK
capture also left `[MOCK_AGENT]` installed, contradicting its own "a BYOK
config has no local agent" premise: an installed agent wins the popover and
renders its model radiogroup instead of the BYOK rows.
Co-authored-by: lefarcen <ontf116@gmail.com>
* Home shell productization batch: message center, What's-new on release data, update-reminder & updater popup restyle (#6162)
* UI polish batch: import-dialog cleanup, settings toast placement, tab chrome fixes
- Plugin/skill import dialog: drop per-card icons, left-align card content
- Settings page: center the autosave pill under the top nav
- Avatar model menu: soften the pinned footer divider
- Workspace tabs: pinned entry tab always reads as Home; remove tab-search
button and its popover
- Model picker: wider row gap in the two-pane browse popover
- Community header: unpin (scrolls with content)
- New update-reminder dialog assets and strings
- Keep entry-settings-button as the signed-out settings entry (e2e contract);
align NextStepActions with the navPlugins key rename
* Update reminder: simulate in-place download progress instead of linking out
The strip's confirm now runs a staged progress state ('updating') with an
eased fake download bar, then marks the version updated — replacing the
jump to the GitHub releases page.
* test(web): retire AvatarMenu account-row suites after the popover simplification
The composer popover was reduced to a model picker (2026-07-24): the Open
Design account row — plan badge, balance, wallet fallback, upgrade/console
links — no longer renders there, so the nine suites asserting that surface
(including the workspace-balance and billing-permission gates that landed
upstream meanwhile) can no longer pass. Drop them and recast the remaining
account-row test as the guard for the new invariant: a signed-in AMR status
must render no account UI in the popover.
Also drop the stale onClose prop from the new upstream sign-out-confirm
test; the rail no longer takes one.
* Design-systems toast: anchor to the pane's top edge, pill radius
* Update reminder strip: progress ring on the button, percent face
While updating, the collapsed strip no longer stacks a label + bar above
the rocket button. Progress draws as a brand-green ring hugging the round
button's edge (5px band reserved so the button never shifts), and the
button face swaps the rocket for the bare percent — 'N%' while
downloading, 完成 (updater.done) at 100. The full 正在下载更新 N% label
stays reachable via the hover bubble and the ring's aria-label. Demo
payload version bumps to 1.4.6 so the once-per-version card re-arms.
* Home shell productization batch: message center in the account menu, What's-new on real release data
- Message center: entry moves into the account menu under 设置 (bell row);
MessageCenter becomes controllable (hideTrigger + open/onOpenChange +
onUnreadCountChange) and mounts persistently in EntryNavRail so unread
polling survives the hover menu; unread shows as a red dot on the account
avatar (and on the menu row); avatars go from squircle clip to 8px rounded
squares; panel filter segmented control and mark-all-read go pill-radius.
- What's-new: the post-update highlights card now wears the shared
release-dialog shell (cover / title / dotted notes / pill footer) while
keeping its REAL /api/whats-new data source, show-once-per-id timing,
soft-hide Escape/backdrop semantics, and analytics. The demo-payload
update-reminder dialog and its simulated download no longer trigger; the
stage machine and progress strip stay for the real updater-feed wiring.
- Rail chrome: brand logo and the in-rail collapse control are gone — the
rail starts at the search box and folding lives in the pinned Home tab's
toggle; the signed-out cloud tip bottom inset now matches its sides.
- Brand picker (modal variant): side-nav split — search + vertical category
nav left, a roomy 2-up borderless gallery right, content flush to the
column line with hover fill/ring bleeding past it; logo tiles lose their
boxed edge; selected template chip and the composer mode pill share the
same selected ring, with the mode glyph optically matched at 13px.
- Tab launcher: rows are icon + label only (descriptions dropped) with a
rotating four-hue icon palette; the 新建空白页面 entry is paused behind
ENABLE_BLANK_PAGE_WORKSPACE_ENTRYPOINT and its PageCreator suites skipIf
on the same switch.
- Move-to-team confirmations: extracted MoveToTeamConfirmDialog, no backdrop
blur (dim only), and the confirm keeps its brand-green label through the
mention-home ink-pill restyle in every interaction state.
- Publish CTA: hover inverts to ink fill with brand-green icon + label.
- Plus assorted in-progress workspace polish (composer, viewer, settings,
model icons) riding the same branch iteration.
* Restyle the updater ready popup to the update-reminder dialog language
Cover art on top (compressed 1296x555 jpg, natural aspect), the orange
accent icon tile removed, secondary action as a soft white pill and the
install CTA as a black pill with brand-green ink, and a stacked footer so
the silent-update checkbox keeps a full-width single line instead of
wrapping beside the widened pills.
* Signed-out rail: message-center entry under 设置
The signed-out rail has no account menu (where the 消息中心 row lives when
signed in), which left the message panel with no opener at all. The rail
item carries the unread dot.
* Composer row: pin chip glyphs against flex shrink
The row-wide min-width: 0 that lets chip labels ellipsize also let SVG
glyphs absorb the squeeze — a hard-narrowed pane rendered the design-system
palette icon tiny. Chips still shrink; their icons keep natural size.
* Bind Community Remix projects to the current workspace
Addresses mrcfps' review thread on apps/web/src/App.tsx:3303.
Product reproduction: 「通过 remix 后的方案, 不出现在我当前 workspace 里的
草稿了」 — stable repro in a team workspace.
`duplicatePluginAsProject` only sends the workspace/member identity headers
when its third `workspaceContext` argument is supplied, and the daemon's
`authorizeCreatedProjectWorkspace` deliberately reads a COMPLETELY headerless
create as a legal legacy/anonymous caller — `bindCreatedProjectToWorkspace` is
a no-op for a null context. Both remix call sites this PR adds (App.tsx's
standalone /community branch and EntryShell's community tab) omitted the
argument, so a remix performed inside a team workspace produced a project
bound to no workspace at all and therefore invisible to every workspace view.
Same enforceWorkspaceProjectMutation / created-project bypass class as
c0bce3b8f, fixed the same way: thread the already-resolved workspace context
that both files already hold, through the repo's existing
`resolvedWorkspaceContextForWrite` seam so an unresolved or unavailable
authority fails closed instead of silently creating an unbound orphan.
The second half of the review note — the follow-up `patchProject` whose `null`
was ignored — was a consequence of the same root cause: with the project
unbound, `enforceWorkspaceResourceMutation` finds no workspace row for the
caller's workspace and 403s the pendingPrompt seed, so the template prompt was
dropped too. Both requests now share one resolved authority, so they can never
disagree about which workspace they are acting in, and a still-null result is
reported instead of swallowed (without creating a second project, which is what
routing it through the existing catch would do).
Red spec first: apps/web/tests/components/community-remix-workspace-binding.test.tsx
drives both call sites in a team workspace against a fetch stub that reproduces
the daemon's create-then-bind contract. Red before this change (the create
carried no workspace headers, the project stayed unbound, and the seed PATCH
403'd), green after.
* Cover the refused-prompt-seed branch of the Community Remix fix
Second half of mrcfps' review ask on the same thread: prove what the flow does
when the follow-up `patchProject` still returns null after the binding fix.
The remaining null shapes are genuine transient refusals (daemon down,
membership revoked mid-flight, workspace locked between the two requests). By
then the copied project is real and bound, so the flow deliberately does NOT
fall into its own catch — that path creates a second, prompt-only project and
would strand the copy. It keeps the user on the remix and reports the dropped
seed. This case pins that: user lands on the remixed project, no second project
is created, and the failure is surfaced rather than swallowed.
* Re-anchor three style specs on the dogfooded UI
`Web workspace tests` went red on this branch because three CSS specs still
described the pre-dogfood layout the restyle replaced. Each one is re-pointed at
what shipped, keeping the invariant it was written to protect:
- `acceptance-visual-fixes` (recvpYDfW12NBu): the preset-thumbnail fix is the
resting `scale(1.15)` framing transform, which is intact. Its hover partner
went away with the family-wide cover-zoom removal (2026-07-27), so the spec
now asserts the framing zoom on both media surfaces and separately locks the
hover rules out — re-adding one would resample the covers soft again.
- `mention-popover`: the filter strip scrolls on one line instead of wrapping to
a second row. The "no clipped labels" half of the contract is unchanged and
still asserted (pills hold natural width, labels stay `nowrap`).
- `settings-polish`: the updater popup footer stacks the silent-update checkbox
above a 50/50 action row. Long en labels must still wrap inside the checkbox
column rather than overflow the panel, which the spec keeps checking.
* Return message-center focus to the rail opener that owns it
`hideTrigger` handed the message-center entry point to the rail but left the
component's internal `triggerRef` unattached, so `closePanel()` restored focus
to nothing. Opening focuses the portaled dialog, so every close — button,
backdrop, Escape, notification-settings — unmounted the focused node and dropped
keyboard focus to the document. Both external openers also failed to advertise
the dialog they own.
`MessageCenter` now takes a `returnFocusRef`: the host that hides the built-in
bell owns the duty that button used to serve. `EntryNavRail` points it at a
control that is still mounted after the close — the account trigger on the
signed-in branch, because the hover menu unmounts the 消息中心 row before the
panel opens, and the rail item itself when signed out. Both openers carry
`aria-haspopup="dialog"` and the live `aria-expanded`.
Red first: `EntryNavRail.message-center-entry.test.tsx` opens from each entry,
closes with Escape and with the close button, and asserts focus lands back on
the host control. All four cases failed before this change.
* Carry the menu popup contract onto the composer quick pills
The 插件 / 设计百宝箱 pills open the standalone `role="menu"` popovers in
ChatComposer, but they arrived without the contract of the ComposerPlusMenu rows
they replaced: no `aria-haspopup` / `aria-expanded`, no Escape handler, and no
focus return. Keyboard and screen-reader users could not tell a popup was open,
and Escape pressed while focus sat in the plugin search did nothing.
The pills live in ChatPane while the popovers live in ChatComposer, so the state
is plumbed both ways rather than duplicated:
- `onStandalonePanelChange` reports which popover is open, and each pill carries
`aria-haspopup="menu"` plus its own `aria-expanded` — the same pair
`ComposerPlusMenu` already puts on its trigger. No `aria-controls`: that
surface does not use one, and this is not the place to invent a second
convention.
- `openDesignToolbox` / `openPluginsPanel` take the opening pill as the
return-focus target. Both popovers move focus inside themselves, so a
dismissal has to hand it back, and the pill is the control the user came from.
- Escape closes through a document-level handler, mirroring ComposerPlusMenu's.
`dismissStandalonePanels` is deliberately only wired to the dismissal paths
(Escape, backdrop). Picking a plugin or an action keeps the plain setters,
because those hand focus to the composer input and pulling it back to the pill
would fight that.
No interaction changes: hover-open, the shared close timer, pill-to-popup
pointer travel and every selection path behave exactly as before.
Red first: `tests/components/ChatPane.quick-pill-popup-contract.test.tsx` runs
both pills through advertise / expand / Escape-from-inside / backdrop-dismiss and
asserts focus lands back on the pill. All six cases failed before this change.
---------
Co-authored-by: lefarcen <935902669@qq.com>
* Converge the visual Design Files prelude instead of clicking once (#6181)
`Playwright visual (settings-workspace)` went red on `[P2] captures the settings
BYOK surface` with a 10s `aria-selected` timeout: the Design Files tab sat at
`false` for all 23 polls with nothing re-clicking it. Seven other captures run
this identical prelude and passed, including the two neighbouring BYOK cases.
The defect is in the oracle, not the product. `prepareVisualWorkspaceFileList`
decided **once** whether to click — one instantaneous `isVisible()` probe of a
file row — and then asserted a state nothing retries. Any interleaving where the
workspace's own tab reconciliation lands around that single click leaves the
assertion permanently unsatisfiable for that attempt, and the lane runs
`OD_PLAYWRIGHT_FULLY_PARALLEL=1` with `retries: 0`, so it reports as a hard
failure rather than a flake.
`activateVisualDesignFilesTab` now converges on the goal state via `toPass`,
driving off `aria-selected` directly rather than through the file-row proxy —
the guard and the assertion were about different things even though
FileWorkspace derives both from one `activeTab === DESIGN_FILES_TAB` expression.
Repeat clicks are safe: the tab's handler is `setPersistedActive(DESIGN_FILES_TAB)`,
which is idempotent.
Why this is not a product regression from #6162: nothing in it touches tab
selection, tab persistence, or row visibility. Its only `design-files.css` change
deletes the hover cover-zoom, which cannot affect visibility, and a diff of
`apps/web/src` for `localStorage|activeTab|openTabs|restoreTab` comes back empty.
The lane's one green run before this failure (`4a6653186`, the #6174 repair of
the unrelated #5971 BYOK rename) is the whole basis for calling the baseline
clean.
Scope is the visual lane only — every caller is in `visual.ts` or
`visual-*.test.ts`, so the UI P0 lanes are untouched.
* fix(web): explain the AMR workspace-scope block instead of a dead send button (#6178)
An Open Design Cloud project run requires a resolved personal/team workspace
authority (21f452ffe). Failing closed there is correct and stays. Failing
closed SILENTLY was the bug: the send button went grey with no reason on
screen and no way out, while Home's equivalent dead end
(checkAmrBalanceGate -> AmrBalanceDialog reason `signed_out`) hands the user
an in-app sign-in. e2e/ui/amr-logout-requires-relogin.test.ts exists to
protect exactly that route, which a disabled composer can never reach.
Adds a classifier (`amrWorkspaceScopeBlock`) that turns "gate closed" into
the remedy that clears it, and a composer-adjacent notice that renders it:
signed_out — the same AmrLoginPill action and `chat.amrBalanceGate.signInCta`
copy Home's balance gate uses, so one identical action clears it from
either surface.
unresolved — a re-read of the project's workspace authority, exposed as
`ProjectWorkspaceScopeState.revalidate`. An account action would be a guess.
The gate itself is untouched: `projectRunWorkspaceScopeReady` never consults
the classifier, and the `disabled: true` invariant stays asserted.
* test(updater): join the installer-reinstall floor across release and client (#6167)
* test(updater): join the installer-reinstall floor across release and client
A payload update never replaces the Electron outer shell, so a release whose
shell changed can only reach an old install through the installer. The
`control.launcher.version.{min,url}` floor is the one mechanism that forces
that, and it spans three owners: tools/release resolves the channel policy,
the feed carries it, and the desktop updater enforces it against the
physically installed outer version. Every side had unit coverage; nothing
joined them, so the only place a break in the chain would surface was a
release publish.
Adds six specs to the already-allowlisted packaged-launcher cross-boundary
suite, driving the real release-side resolver into a real feed and then into a
real packaged update check:
- an outer below the floor takes the installer, not the payload, and the
launcher pointer proves nothing was adopted;
- the floor is judged by the installed outer rather than the payload it is
running (two installs on the same payload, only the stale shell diverted);
- an unreadable installed outer fails closed as `outer-version-unreadable`
with no installedVersion to render;
- a launcher schema beyond this client diverts on the ABI axis alone;
- with no floor configured a below-floor outer still swallows the payload,
which is why a shell-changing release must set the repo vars deliberately;
- a floor above the release version stays refused.
Falsifying `remoteRequiresReinstall` to compare against `config.currentVersion`
-- the semantics the released 0.15.1/0.16.1 shells actually ship -- turns two
of these red, including the unreadable case being mislabelled as below-min.
Also pins the floor's operational shape against real channel version formats:
a bare stable floor sorts above every same-base `-beta.N`/`-preview.N`/
`-prerelease.N`, so inheriting it into those lanes hard-fails their publish.
The specs name that trap and pin the one-explicit-pair-per-lane configuration
that avoids it.
* test(updater): drive the reinstall floor on Windows too, off the real outer
Review (nettee) caught these specs running macOS only: the feed described
`platforms.mac`, the payload writer built a `.app`, and the updater config was
pinned to arm64/darwin. So `resolveInstalledOuterVersion`'s Windows branch —
`dirname(launcherLaunchPath)/resources/open-design-config.json`, a different
lookup from the mac bundle path — and the `installer` artifact shape never ran.
Since the installs most exposed to a shell/payload mismatch are the oldest ones,
leaving Windows uncovered left the half most likely to break unguarded.
The specs now run through `describe.each` over a per-platform target table
carrying the launch-path shape, outer-config location, installer artifact key,
and payload extractor.
They also stop using `OD_UPDATE_INSTALLED_VERSION`. That override returns before
the platform branch it was supposed to exercise, so it was masking the code under
test. Each scenario now materializes a real installed outer package at the real
per-platform location and lets the updater read the version off disk — which
also makes the unreadable-outer case genuinely unreadable rather than simulated.
Falsifying the Windows lookup (collapsing it into the macOS path) fails exactly
the two Windows specs that depend on reading a real installed version, while all
twelve macOS and publication-policy specs stay green.
* fix(web): partition the workspace plan nameplate like the wallet (#6182)
A workspace Vela Web shows as 免费 rendered as 专业版 Plus in the client's
account menu, while the 额度 row in the same card correctly read $0.00.
Money and plan travelled different paths. `GET /api/workspace/billing`
returns three independently scoped things, and only two of them are about
the workspace: `workspaceBalance` / `workspaceSnapshot` are proven for the
exact workspace + member, while `summary` is the caller's VELA ACCOUNT
billing (`workspaceId: null` by contract; the daemon reads it with one
unscoped `fetchBilling()` regardless of `?workspaceId=`). The rail's plan
nameplate read `response.summary` raw, so an account holding a personal
Plus reported `plus` in every workspace — a value that cannot change when
the workspace changes, because it was never about the workspace.
`workspaceBillingSummaryForContext` makes the partition key explicit, the
same `workspaceId` + `workspaceMemberId` pair money already uses:
- personal workspace — the account IS the scope, summary passes through;
- team workspace — plan comes from the context-authorized snapshot, and
the account tier may stand in only when it is itself team-namespaced,
since a personal tier cannot name a team workspace's plan.
That last clause keeps the 飞书 P0 fix intact: B omits planId/billingState
for a non-owner and `workspaceSnapshot` is an additive capability, so a
team-namespaced account tier is the only surviving evidence that a paying
MEMBER's team is subscribed.
`useWorkspaceBilling` is now this projection over the ambient context, and
EntryShell / SettingsDialog consume it instead of the raw summary. No
interaction, layout, or copy changes.
* Revert "fix(web): explain the AMR workspace-scope block instead of a dead send button (#6178)" (#6184)
This reverts commit ea259eb402.
* fix(daemon): resolve an unbound project against the caller's current workspace (#6185)
* fix(daemon): resolve an unbound project against the caller's current workspace
Every project must resolve to a workspace; when the project itself has no
binding, the workspace is the one the caller is currently acting in. There is no
other candidate.
`GET /api/projects/:id/workspace-scope` read only the persisted
`workspace_projects` row and the membership directory — never the request's
`x-od-workspace-*` identity — so a project with no row answered `unbound` for
every caller forever. `unbound` makes `projectWorkspaceScopeAuthorizesAmr` false,
which disabled the chat composer's send button for an Open Design Cloud run on
that project permanently, with nothing the user could do to clear it. #6178 wrote
user-facing copy for that state instead of fixing it (reverted in #6184).
`resolveProjectWorkspaceScopeForCaller` wraps the existing resolver instead of
branching inside it, so `resolveProjectWorkspaceScope` stays byte-identical and
the bound path is provably untouched. The fallback re-enters that resolver with a
synthetic binding naming the caller's own workspace, so the resulting
`workspaceMemberId` comes from the membership directory and never from the
request header: a caller can only select among workspaces their signed-in
identity is genuinely an active member of, and anything the directory cannot
confirm degrades back to `unbound` rather than to `unavailable` on a workspace the
project was never bound to.
Two cases keep today's behavior, both pinned by spec:
unavailable — all three of its return sites. A project pinned to workspace X
read by a member of Y answers X. The scope carries `workspaceMemberId`, the
wallet that pays for the project's runs; resolving to Y would bill Y for X's
project. Reachable because `GET /api/projects/:id` has no workspace gate, so
a deep link or a workspace switch lands there.
no caller identity — signed out, or a plain curl, has no current workspace to
fall back to, and inventing one is worse than answering "none".
The fallback deliberately does not persist a binding.
`reconcileUnboundProjectBeforeMutation` was the preferred shape going in and is
wrong here: its own docblock is explicit that a passive read must not hand out
ownership just because it ran first, and this endpoint is a GET. Writing a row
from it would let whichever workspace opened the project first claim authorship
on no user intent, and would race two clients in different workspaces.
* fix(web): stop pre-emptively blocking an AMR send that has a wallet
An Open Design Cloud run is billed to the CALLER's own wallet, so the only
defensible client-side veto is "there is no billing principal at all". The gate
instead required THIS PROJECT's workspace scope to resolve, and blocked the send
whenever it did not: an unbound project, a membership-directory read that
transiently failed (offline / 504 / timeout all collapse into one `ok: false`
upstream), a workspace that is billing_past_due or locked, or a team the caller
has since left. In every one of those the user is spending their own quota.
It is also not the enforcement point. Real enforcement is server-side — the
daemon's `WORKSPACE_CONTEXT_REQUIRED` 401 plus vela's own billing check. The
client gate could only convert a request the server would have answered into a
dead, unexplained button, which is strictly worse than an honest server error.
It arrived as 21f452ffe, whose entire message is "fail closed on unresolved
workspace authority" with no body and no stated product requirement.
`workspaceIdentityCanBillAmr` names the invariant on the identity read, and the
gate now admits either witness of a billing principal: that identity, or a
project scope that already resolves to an explicit personal/team principal.
Strictly a widening — every state it newly admits was previously blocked, and
nothing previously admitted becomes blocked.
Deliberately NOT treated as "no wallet":
loading — the identity read holds no answer yet. Reporting "signed out" on a
frame that has not heard back is the bug shape this replaces.
failure 'unavailable' — a transient outage taught us nothing about the user.
failure 'unsupported' — an old daemon with no workspace endpoint keeps its
legal pre-workspace behavior.
A genuinely signed-out caller — a settled, authoritative read that came back with
no workspace — stays blocked: there is no wallet, so the run cannot be billed and
cannot succeed.
No UI, no notice, no copy: #6178 wrote copy for this dead end instead of removing
it, and #6184 reverts that. This removes the dead end.
* fix(web): point every console entry at /dashboard, drop the wallet target (#6165)
* fix(web): point every console entry at /dashboard, drop the wallet target
Product ruling: there is no wallet page in the console's information
architecture any more. Balance, manual top-up and the auto-recharge policy were
all rehomed onto the console dashboard (vela #1055), so every entry that used to
open `/wallet` now dropped users on a surface the product no longer navigates
to.
Retargets all of them:
- `AMR_CONSOLE_URL` / `DEFAULT_AMR_RECHARGE_URL` and the per-profile console
map (prod / test / local) now resolve `/dashboard`, keeping the
`?source=open_design` attribution param.
- `teamConsoleUrl`'s `billing` section — the team account menu's 「额度 $0.00」
row — is a plain dashboard visit.
- `teamConsoleUrl`'s `plans` section, `amrPlansUrlForProfile` and
`amrPlansUrlForWorkspace` — the free-tier 「升级」 button and every other
Upgrade affordance — send `billing=plan` instead of the wallet page's
`view=plans`.
- `amrWalletUrlForWorkspace` is renamed `amrConsoleUrlForWorkspace` so the name
matches where it now points.
`billing=plan` is the console's one state-aware upgrade intent: its dashboard
resolves it against the workspace's real subscription state, so a personal owner
lands on the personal plan modal — the dialog the console's own 「升级订阅」 hero
button opens, which is what product asked the free-tier 「升级」 button to reach —
while a team owner still gets checkout or change-plan. The team `upgrade` section
keeps sending its more specific `billing=checkout` hint, but that is now only a
hint: powerformer/vela#1127 makes the console fall back to the dialog that
actually matches, so a stale `hasActivePlan` can no longer strand the user on a
bare Overview page (recvpSQKna0LwR).
Comments carrying earlier live-verification findings are updated wherever this
change invalidated them, rather than left contradicting the code.
No new user-facing copy, so no locale changes: the affected labels are all
action-named (「充值」/「升级套餐」), never destination-named.
* fix(daemon): retire the wallet target in the AMR recharge link too
The daemon builds its own recharge URL for AMR balance failures — it lands in
the failure message and in `actionUrl`, which the client renders as the chat
error card's clickable link — and it still pointed at the console's retired
wallet page. Same product ruling as the client-side retarget: balance and manual
top-up report on the console dashboard now (vela #1055), so this link has to go
there.
Every existing assertion in `vela-errors.test.ts` referenced the constant
symbolically, so all of them would have kept passing while the link sent users
to a surface the product no longer navigates to. Added one that pins the literal,
which is the red spec for this change.
Also retargets the two e2e assertions that pinned the old destination
(`e2e/tests/amr/insufficient-balance.test.ts` on the daemon's emitted URL,
`e2e/ui/visual-workspace.test.ts` on the avatar upgrade link's `view=plans`), and
the web-side fixtures that mimic the daemon's emitted string so they stop
documenting a URL it no longer produces.
* chore(release): bump feat/workspace-team to 0.16.2 (#6188)
The published beta feed is self-inconsistent: metadata.json carries
baseVersion 0.16.1 alongside betaVersion 0.16.2-beta.144, because
0.16.2-beta.144 was published with an explicit release_version override
while every manifest on this branch still said 0.16.1. Every subsequent
beta build now fails its own consistency check:
[release-beta] beta metadata.json baseVersion 0.16.1
does not match betaVersion 0.16.2-beta.144
`force` does not help — it only waives "base must exceed latest stable",
not the baseVersion/betaVersion match.
Raising the branch's base version to 0.16.2 makes the next beta
(0.16.2-beta.145) consistent with its own metadata, and removes the need
to pass force at all. Same 18 manifests the release bot touched in
23f9fcaac; test fixtures that mention 0.16.1 as data are left alone, as
that commit also left them.
Co-authored-by: lefarcen <ontf116@gmail.com>
* fix(collab): keep a project's "last changed" time out of sync's hands (#6189)
A member who opened the client hours later and pulled a shared project saw
「刚刚更新」 on a project nobody had touched, and the home 最近项目 strip kept
rendering the previous workspace's cards for a beat after switching workspaces.
Two independent defects, both reported by the product owner on a packaged
client.
1. `projects.updated_at` (and the `workspace_projects.updated_at` the project
list folds into it with `MAX`) is what the project card shows and what the
list sorts by, so it must answer "when did a person last change this
project's conversations, files, or name". Sync was answering it too:
`materializePulledTeamMirror` carried the origin's timestamp into the
project row but not into the binding it wrote in the same transaction, so
`MAX` surfaced the pull's own clock on EVERY pull. Four more sync writers
had the same shape. `SYNC_KEEPS_UPDATED_AT` names the invariant and the
sync paths now either carry the origin's time or keep the row's existing
answer.
2. A loaded project list describes exactly one workspace identity, so leaving
that workspace invalidates it. The re-list on switch runs in an effect —
after the commit — so the browser painted workspace A's cards under
workspace B's identity first. The list is now dropped during the render
that first observes the new scope, so no frame shows another workspace's
projects. No extra request: the refetch already existed and was already
keyed on the resolved workspace.
Co-authored-by: lefarcen <ontf116@gmail.com>
* fix(web): key every workspace read cache on who is asking (#6198)
`coalesced-get` and `shared-cancellable-get` are CACHES (1s share window), not
just in-flight dedupers, so a key that omits the caller's identity hands the
previous workspace's answer to the next one — and an in-flight entry is joined
with no window at all.
Three reads were keyed on a constant:
- `useTeamMembers` — `'workspace-members'`. `GET /api/workspace/members` answers
for whichever workspace the DAEMON is active in and reads nothing off the
request, so the roster is a per-workspace answer with no per-workspace URL.
A leaked roster renders the previous workspace's teammate name on project
cards (`RecentProjectsStrip.resolveCreator`). Now keyed on the workspace
identity, which also makes the hook re-read on a switch instead of waiting out
its 15-60s poll, and a late read for the identity the user left no longer
overwrites the newer roster.
- `useWorkspaceContext` — `'workspace-context'`. This is the read that
ESTABLISHES the identity, so it cannot be keyed on the identity it fetches and
has no workspace argument to borrow (the switch is a separate
`PUT /api/workspace/active`). It is keyed on the identity GENERATION instead:
one token per deliberate identity change, so a single broadcast heard by a
dozen mounted consumers still collapses to one request, while two switches
inside `forceCoalescedGet`'s 250ms burst window stop being mistaken for one.
- `useProjectWorkspaceScope` — `project-workspace-scope:<id>`. The scope carries
the `workspaceMemberId` that pays for the project's runs, so a shared key
serves one member the wallet of another.
The same read also fetched HEADERLESS, which made #6185's daemon fallback
unreachable: `GET /api/projects/:id/workspace-scope` resolves an unbound project
against the caller's current workspace, but only when the request carries the
caller's `x-od-workspace-*` identity. It now sends `workspaceProjectHeaders`,
the same set every other workspace-aware project read uses — and waits for the
caller's own identity to settle first, so a project open still costs one request
instead of a wrong headerless one followed by a corrected one.
* fix(collab): carry B's workspace name for personal workspaces too (#6199)
B names EVERY workspace, personal included: vela's CurrentWorkspaceContext
requires `workspaceName`, derives "«owner»'s workspace" for an unnamed
personal one (vela #964), and an owner may rename it outright.
The daemon read that field and then threw it away unless the workspace was a
team, because the only name on WorkspaceCollabContext was `teamName`. The
switcher's collapsed label therefore had nothing real to show for a personal
workspace and fell back to a hardcoded English "Personal workspace" until the
user opened the dropdown and the separate workspace-directory read landed.
Adds `workspaceName` to the context contract and carries it for both workspace
types, in the vela mapping and in the directory-item synthesis. `teamName`
stays team-only: it is the team switcher's field and doubles as an "is a team"
signal, so nothing may start reading it for a personal workspace.
The web side consumes the context the shell already holds at startup, so the
label is correct on first paint and NO new request is added — the diff adds
zero fetch call sites. The dev provider parses `workspaceName` too so the
dev/demo lane can drive a personal workspace's label the way B does.
Note this is not what the reported screenshot shows: there, B's real name for
that workspace IS the literal "Personal workspace", because an in-place team
upgrade flips the default workspace's stored type to 'team' and vela's
owner-derived naming then no longer applies. That half is upstream.
Co-authored-by: lefarcen <ontf116@gmail.com>
* fix(web): stop offering 升级 at the top plan tier (team_max) (#6205)
A 团队版 Max workspace is the top of the ladder — nothing is above it — but
both upgrade entry points still showed 「升级」, and clicking it could only
reopen the plan the user already holds.
The two surfaces got there two different ways, and neither shared a rule:
- The account menu's billing card gated on `billingUpgradeUrl &&
canManageBilling` — a destination check and a permission check, with no
TIER check at all. Every tier saw the button.
- The Settings AMR card asked a personal-ladder question
(`canUpgradeVelaPlan`) about `amrCardStatus.account.plan` — vela's
ACCOUNT-scoped login projection, which reports `free` for a user whose
entitlement is held by a team workspace. So a team_max owner measured as
"free" and got the button, while the PlanBadge beside it correctly read
Max off the workspace-resolved tier.
Both now ask one predicate, `canUpgradeFromPlanTier`, about the same
resolved tier each surface already LABELS, so the button can never
contradict the nameplate next to it.
Team and personal tiers are two ladders, so the predicate reads the
team-namespaced id whole instead of stripping the prefix and asking a
personal question. Per the product ruling — 「个人档位都是要显示可升级的,
最顶的就是团队 max」 — personal `max` keeps the affordance: that user can
still move onto a team plan, and vela #1146 deliberately routes their
click to the Team upgrade dialog. Only `team_max` loses it.
Destination logic (`workspaceUpgradeUrl` / `amrPlansUrl`) is untouched;
this only decides whether the affordance renders. No copy, no new i18n
keys.
* fix(web): keep Send alive after using a community template (#6206)
Clicking 「使用」 on a community template seeds the template's own brief into
the composer and binds the template as the run's driver. For a template whose
`od.inputs` are `required: true` with no `default` -- the real
`example-html-ppt-pitch-deck` ships three such fields -- the bind left
`active.inputsValid` false, and `submitDisabled` folded that into the send
button. The user saw a full prompt next to a permanently dead Send whose
tooltip still read "type something to run". Typing did not help: the gate was
never about the prompt.
Those fields have no input surface on that path. #3645 removed the Home
composer's inline plugin-inputs form, and the template's query carries no
`{{...}}` placeholders, so prompt -> inputs write-back cannot reach them
either. A gate on values the user cannot supply anywhere is a dead end, not a
guard.
Narrow it to where it is actionable: `requiredInputsAreUserFillable` keeps the
gate when the media composer renders the fields as controls, or when the
tracked query template actually names the missing field as a `{{placeholder}}`
that editing the prompt writes back. A tracked template alone is not enough --
re-applying a plugin without an explicit template derives one from the
manifest, placeholders or not, which is the shape a reload lands in.
The daemon stays the authority: `validateInputs` still throws
MissingInputError. Since the seeded path no longer pre-empts the send, its
rejection now reports the fields it rejected instead of a generic "Failed to
apply", reusing the existing pre-submit wording.
* fix(collab): give every created project a workspace home (#6201)
* fix(collab): give every created project a workspace home
A project created with no `x-od-workspace-*` headers got no
`workspace_projects` row and still returned 200, so nothing surfaced the
loss. `GET /api/projects/:id/workspace-scope` then answers `unbound`,
which strips the workspace off the run request (`ProjectView`'s
`projectRunWorkspaceContext` -> an Open Design Cloud run nothing can
bill) and blanks the balance/plan area while that project is open
(`AvatarMenu`). The same orphan is also denied its first run outright by
`enforceWorkspaceResourceMutation`.
Headerless creates are not anonymous callers asserting "no workspace" —
they are callers that could not say which workspace they meant:
`od project create` and the MCP `create_project` tool mint no headers at
all, the web duplicate / design-system-copy / plugin-share paths read a
ref that drops the context's `loading` flag, and Orbit + scheduled
routines have no request to read headers from. So a create that names no
workspace now binds to the one this daemon is signed in to, read through
`workspaceContext.lastKnown()` — zero-network, synchronous, no failure
mode. A daemon that has resolved no workspace still binds nothing, which
keeps `unbound` correct for the signed-out single-player user.
Six creation paths were writing no binding at all, not merely skipping it
when headerless: the plugin share-project task, Orbit run projects,
scheduled routine projects, the library capture-as-page exit, the
project-location scan importer, and the `ds-*` project backing a design
system's editing workspace. All six now bind.
Deliberately unchanged: the collab pull's register-on-pull and
shared-project placeholder. Those are a teammate's shared project, whose
correct binding is written by `materializePulledTeamMirror` with
`visibility: 'team'`; claiming them into the reader's ambient workspace
as personal would misattribute someone else's resource.
No creation path gains a new failure mode. `createdProjectWorkspaceHome`
never refuses — a partial or denied header identity degrades to the
ambient workspace rather than turning a working 200 into a 4xx.
On the web side `createPluginShareProject` was the one create in
`state/projects.ts` with no `workspaceContext` parameter at all, so it
sent no workspace identity ever. It now forwards the context when known,
best-effort — deliberately not `resolvedWorkspaceContextForWrite`, which
throws while the identity read is in flight and would add a new block to
a path that works today.
* fix(collab): verify an asserted workspace before persisting it
`createdProjectWorkspaceHome` resolved a created project's workspace with
`resolveCreatedProjectWorkspace`, which only PARSES headers. It trusted
the caller-provided member status and permissions and never checked that
the workspace/member pair exists in the membership directory or agrees
with the daemon's last-known state. `x-od-workspace-*` headers are an
unauthenticated hint, so any local caller — the `od` CLI, a plain curl, a
compromised page — could assert an arbitrary pair and get a project bound
to a workspace it has no membership in, with a fabricated
`createdByWorkspaceMemberId`.
"Do not fail the create" and "trust unverified input" are not the same
constraint. The helper now VERIFIES, then DEGRADES — never rejects, never
trusts:
1. asserted identity that verifies -> bind to it
2. asserted identity that does NOT -> bind to the daemon's ambient
verify, including an unreadable workspace instead, or leave
authority ("cannot confirm") the project unbound
3. nothing asserted -> ambient, as before
Creation still never answers 4xx, so the six previously-never-bound paths
keep binding and a signed-out daemon still creates usable local projects.
An unverifiable claim is simply never written as fact.
Verification is not re-implemented. It is `authorizeCreatedProjectWorkspace`
— the same directory lookup `POST /api/projects` gates on, which also
returns the DIRECTORY's authoritative context rather than the caller's
claimed one — plus `withLastKnownMembership`, the same cross-check the
mutation gates apply (now exported rather than copied). Only the failure
behavior differs: they refuse, this degrades. An absent directory fetcher
remains `authorizeCreatedProjectWorkspace`'s own documented local/dev
compatibility path, not a second weaker definition of verified.
Route modules now take one `CreatedProjectWorkspaceResolver` dep instead
of re-threading the three authorities, so there is a single notion of
"verified" per daemon.
* test(daemon): drive brand routes through the created-project workspace resolver
The brand harness constructed `registerBrandRoutes` without the new
`resolveCreatedProjectHome` seam, so the binding became a no-op and
"binds a freshly extracted brand project into the caller's ACTIVE team
workspace" went red. Inject the same production resolver `server.ts`
builds, in its no-directory (local/dev) configuration, so the test keeps
asserting the product behavior through the real seam instead of a
header-trusting shortcut.
---------
Co-authored-by: lefarcen <ontf116@gmail.com>
* fix(web): move the update-ready rocket above the rail account row (#6208)
* fix(web): move the update-ready rocket onto the rail account row
#5517 removed the entry topbar and parked the updater host in the rail
footer, which put the green rocket on its own line UNDERNEATH the account
row — bottom-left, detached from the identity it belongs to. Product
reported it from a real client: the rocket belongs at the right edge of
the name + plan wordmark line.
Move the mount instead of nudging it with CSS: the rail now owns the
updater host through an `updaterSlot` prop and renders it inside the
account row (`.entry-nav-rail__account-row`), as a SIBLING of the account
trigger so it never steals the account-menu click target. The rail footer
stays as the fallback home for the signed-out shell, which has no account
row to ride, so the rocket's appearance conditions are unchanged.
Same change also un-clips the 「更新已就绪」 prompt. The host is pinned to
the bottom of the rail, so a `top: 0` panel grew downward off the bottom
of the window and the copy was cut off mid-sentence. Anchor its BOTTOM
edge to the host instead, the same way `.entry-help-popover` already does
for the same reason.
Side effect worth having: the rail no longer grows a 52px footer strip
the moment an update lands, so the account row stops jumping.
* fix(web): put the rocket ABOVE the account row, right-aligned
Placement correction. 「放到名字啥的上面的右侧」 was read as "on the name
row, right side"; product clarified 「应该是在名字那个横条的上方,靠右对齐」
— the rocket gets its own right-aligned strip ABOVE the identity row, not a
slot inside it.
The strip (`.entry-nav-rail__account-updater`) is mounted unconditionally so
it stays the account trigger's immediately-preceding sibling, and carries
`:empty { display: none }` so an idle rail reserves no height for it. That
rule is load-bearing: without it every user without a pending update would
get a blank 34px row plus the container's 6px gap pushing the account area
up — a worse regression than the placement it fixes. Measured in a real
browser at 1280x900: the account trigger sits at y 850-884 in BOTH states,
so nothing shifts.
Everything else from the first commit stands: the rail owning the host via
`updaterSlot` instead of an opaque `footerExtra`, the sibling-not-child
relationship to the trigger (now moot in the best way — a strip above the
row cannot be button-in-button), the `bottom: 0` panel anchoring that
un-clips 「更新已就绪」, and the footer fallback for the signed-out shell.
* fix(web): stop the home model list offering picks the chip cannot honor (#6209)
On the Home composer, clicking a model in the chip's compact list did
nothing — the chip kept showing the previous model.
The compact list rendered `currentAgent.models` raw, so a model above the
caller's plan (`enabled: false`, straight from `vela model list --json`)
appeared as an ordinary selectable row. Clicking it wrote the pick to
config, `normalizeAgentModelChoice` reported the plan fallback instead, and
the re-normalization effect in `InlineModelSwitcher` persisted that
fallback — so the chip snapped back with no explanation, and the user's
previous pick was overwritten by the default in the process.
The sibling surface (`AvatarMenu`, the project composer's model list) had
already hand-rolled `agent.id === 'amr' && model.enabled === false` for
this. Copying the rule per surface is what let the newest list drop it, so
this introduces one definition instead: `agentModelIsSelectable`, defined
to be at least as strict as `normalizeAgentModelChoice` (pinned by a test)
so a gated row can never be a pick the config would revert. Both surfaces
now ask it, and every model write inside `InlineModelSwitcher` — the
compact list and the execution-settings picker — goes through a single
`applyAgentModel` sink that asks the same question, leaving no second path
for a future list to bypass.
Locked rows now read as locked: dimmed, `aria-disabled`, a lock glyph, the
existing `settings.amrModelUpgradeHint` reason, sorted below the available
models, and a click routes to the plans page instead of silently doing
nothing.
* fix(collab): verify the asserted workspace before claiming an orphan project (#6213)
* fix(collab): verify the asserted workspace before claiming an orphan project
`reconcileUnboundProjectBeforeMutation` claims a project this daemon has
never bound to ANY workspace into the current mutating request's
workspace, so a true orphan is not denied its first mutation by
`enforceWorkspaceResourceMutation`'s `!row -> false` rule (recvqbhor3pai2).
It resolved that workspace from `workspaceProjectContextFromRequest(req)`
— header PARSING with no authority check — and stamped
`createdByWorkspaceMemberId: ctx.workspaceMemberId` from the same headers.
`x-od-workspace-*` is an unauthenticated hint any local caller can forge,
so a plain curl could claim someone else's orphaned project into a
workspace it has no membership in AND install itself as the author.
Authorship is the dangerous half: `workspaceResourceAccess` derives
`selfCreated` from it, and that bit is what grants a non-privileged member
mutation rights over the row.
Both the workspace and the authorship now come from
`resolveCreatedProjectHome` — the same verify-then-degrade resolver #6201
introduced for created projects:
- asserted identity VERIFIES against the membership directory -> claim
it, attributed to the DIRECTORY's member id, not the header's;
- it does NOT verify (foreign, inactive, permissions disagree,
last-known says removed, or the authority is unreadable so it cannot
be confirmed) -> claim the daemon's own ambient workspace instead,
attributed to that verified session's member id;
- neither available -> write nothing, and let the pre-existing gate
answer.
The `null`/`'missing'` early return is unchanged and load-bearing, which
is why this does not reuse `createdProjectWorkspaceHome`'s own third
branch: the mutation gate's HEADERLESS branch answers 401 as soon as any
row exists for the resource, so claiming on a request that asserts
nothing would convert a working headerless mutation into a 401.
A verified caller is unaffected — `workspace-projects.test.ts`'s
recvqbhor3pai2 regression case still passes unchanged. A caller that
cannot prove membership over a never-claimed project is now refused by
that pre-existing gate instead of being let through on a binding it had
just invented for itself; #6201's e2e case is updated to assert the
persisted binding rather than the mutation's status, since both outcomes
satisfy the property it pins.
* fix(collab): persist only an asserted-and-confirmed workspace on an orphan
Review follow-up (mrcfps). `resolveCreatedProjectHome` is not a pure
verifier: by contract a foreign, inactive, removed, or unconfirmable
assertion degrades to `workspaceContext.lastKnown()`. Reconciliation then
persisted that ambient context onto a PRE-EXISTING orphan before the
mutation gate had rejected the asserted caller — and the
`getWorkspaceProjectByProjectId` guard one line above makes that write
sticky, so the rightful verified workspace could never reconcile it
afterwards. A forged request, or a legitimate one during an authority
outage after the active workspace changed, would permanently mis-file the
project.
Creation and reconciliation are not the same risk, which is the
distinction the original patch missed. Putting a BRAND-NEW project in the
ambient workspace takes nothing from anyone — that is #6201 and it stays.
Putting an EXISTING orphan there may take it from the workspace it
actually belongs to.
So a degraded ambient authority is persisted only when it names the very
pair that was asserted. Verified assertions resolve to the asserted pair
by construction (the directory lookup is keyed on it), so they are
unaffected; outage continuity still works for the ordinary legitimate
caller, whose client took its headers from this same daemon and therefore
agrees with it; a mismatch writes nothing at all.
Not in tension with 「所有 project 都应该能找到 workspace」: the
display-side fallback (#6185) already renders an unbound project inside
the caller's current workspace, so nothing reads as "unknown". The
binding row is a separate, durable fact and stays conservative.
Adds the fixture-matrix case the review asked for — a populated but
DIFFERENT ambient workspace, asserting the orphan stays unbound. The mock
authority's `/current` is now flippable so the fixture can create a true
orphan while ambient is absent and then populate ambient deterministically
through `GET /api/workspace/context`, rather than depending on a poll tick.
---------
Co-authored-by: lefarcen <ontf116@gmail.com>
* fix(web): use neutral Windows app background (#6218)
* fix(collab): let a headerless caller mutate its own daemon's project (#6216)
* fix(collab): let a headerless caller mutate its own daemon's project
`enforceWorkspaceResourceMutation`'s headerless branch answered
401 WORKSPACE_CONTEXT_REQUIRED as soon as ANY `workspace_projects` row
existed for the resource. That was survivable while headerless creates
left projects unbound. #6201 made every created project get a workspace
home, so the two rules combined into a project its own creator cannot
touch:
od project create -> 200, and now writes a binding
od project duplicate -> 401
The `od` CLI never sends `x-od-workspace-*` — only `od workspace …`
builds those headers — so this is not an edge case for it. It is every
CLI mutation of every project the CLI created, which breaks the
embeddability contract `AGENTS.md` makes the CLI responsible for.
Reproduced end to end through the real binary against a daemon-only
process (`od daemon start --headless`, no web, no browser).
INVARIANT, now named in `headerlessMutationAllowed`: every mutation
resolves to a workspace identity. A request that ASSERTS one is judged on
that assertion; a request that asserts NOTHING is the local daemon's own
signed-in user and is judged as that identity. Being unable to name a
workspace is not the same as having no standing in one — the same
"nothing asserted -> ambient" fallback the create path already applies,
so the gate and the creation paths finally agree about what a headerless
caller is.
Verified the ambient identity is available without any UI: a
daemon-only process resolves it from the vela session on disk, and
`GET /api/workspace/context` answers fully in that state. The
`ambientWorkspaceId` in `collab/workspace-hub-subscriptions.ts` is
unrelated — it is SSE subscription bookkeeping fed from the
disk-persisted selection store, not an authorization input.
Deliberately NOT weakened: `authorizeCreatedProjectWorkspace` still
refuses to let ambient stand in for an explicitly CLAIMED pair, and
`resolveProjectWorkspaceScope` still resolves a persisted binding without
consulting ambient. Both govern cases where something WAS asserted;
headerless is the third case.
What stays refused, because the original branch protected something real
(recvqbeDjAsejl / recvqbklNGDqYY, spec 04 §10): a resource bound to a
workspace the daemon is not currently in; a resource the daemon's own
identity may not mutate anyway (the same
`workspaceResourceMutationAllowed` computation runs); and everything when
the daemon has no signed-in identity to resolve. With no ambient wired
the branch is byte-identical to before.
The ambient snapshot type moves down into
`collab/workspace-resource-mutation.ts` so the gate and the creation
paths cannot drift into two notions of "the daemon's own workspace";
`created-project-workspace.ts` re-exports it and now reuses the shared
context builder instead of its own copy.
* test(e2e): pin that signed out, visible equals unbound equals mutable
「未登录也可以用自己 cli 修改未登录态下的那些 project, 这个一定要保证,
不然就是 P0 事故」
A signed-out client can only SEE unbound projects: the no-scope catalog
(`routes/project/index.ts:2432-2443`) reads no `x-od-workspace-*` at all
and joins through `listUnboundProjects`, so a project any workspace has
claimed never leaks to a caller with no identity to check it against
(spec 04 §10). This pins the other half of that set — everything visible
while signed out stays WRITABLE.
The case runs against a daemon with NO vela session at all, not merely a
request without headers, creates its draft in that same signed-out state
(the real user sequence), asserts the draft is listed by the no-scope
catalog, then renames it, duplicates it, and duplicates it again through
the real `od` binary.
It holds only because `headerlessMutationAllowed` short-circuits on "no
row anywhere" BEFORE it asks for an ambient identity, so it is
mutation-tested against that ordering rather than written red-first:
moving the short-circuit after the ambient check turns it red with
`expected 401 to be 200`.
* fix(collab): make the delete side effect use the gate's own context
Review follow-up (mrcfps). Allowing a headerless caller to mutate a
project the daemon's own identity owns made a latent inconsistency
reachable: `DELETE /api/projects/:id` re-derived its context with
`workspaceProjectContextFromRequest(req)`, which is null for a headerless
caller, so `requestTeamVisibility(..., 'personal')` was skipped while
`dbDeleteProject` still ran. The owner's local row and directory
disappeared and the hub kept serving the resource, so teammates went on
seeing a project that no longer existed — a cross-client data-consistency
bug, worse than the 401 this branch set out to fix.
Threading the context, not narrowing the capability:
`effectiveWorkspaceProjectContext` names the identity a mutation actually
acts under — asserted when the request carries one, ambient when it does
not — and its docblock states the invariant that any SIDE EFFECT must read
from it rather than re-deriving from headers, or the effect disagrees with
the gate that permitted it. The delete now uses it.
The no-context case becomes a refusal rather than a silent skip. It is
unreachable while the gate is intact (a team-bound row is admitted only
for an asserted identity or a matching ambient one), but falling through
is what produced a local-only delete of a still-shared project, so a
future gate change must not be able to reintroduce it quietly.
Audited every other capability this branch newly admits for a headerless
caller. Delete was the only one: `/move` and both share/unshare routes
each guard with their own `if (!ctx) return sendMissingWorkspaceContext(res)`
so headerless never reaches their hub work, and the upload route's
`workspaceProjectContextFromRequest(req) !== null` only shapes a 404 for a
missing project. Rename/duplicate/writeFiles/comment have no hub side
effect.
Adds the requested route regression to
`tests/routes/project-delete-unshares-team-share.test.ts`: real route,
real collab runtime, mutable fake hub, no request headers,
`workspaceContext.lastKnown()` populated — asserting the unpublish and
catalog removal happened and the project is gone locally only after. Plus
the other half: with no ambient identity either, the delete is refused and
nothing is destroyed. Mutation-tested — restoring the header re-derivation
turns the first red with "expected 0 to be greater than 0".
---------
Co-authored-by: lefarcen <ontf116@gmail.com>
* fix(web): draw the team wordmark for every tier of a team plan (#6219)
The account row of a team workspace showed the PERSONAL Plus wordmark. Product
ruling: 「团队版的订阅,这里应该都显示 team 的标识」…「产品期望团队从 free 到
max,徽标都显示 team 的那个,个人的还是维持现状不要动」.
The badge names the plan FAMILY, not the tier inside it, and it got that wrong
from both ends. `planBadgeTierForLabel` asked `plus` / `pro` / `max` BEFORE
`team`, and B namespaces team ids by EMBEDDING the personal tier word — so
`team_plus` matched `plus` and the `team` branch was reachable only from a bare
`team` id, the one team tier that happened to work. The free team tier missed
from the other side: B reports an unsubscribed workspace with a null `planId`
and an empty `membershipTier`, an id no different from a personal free account,
so no id can name it.
`planBadgeTierForWorkspace` states the whole rule once, and both surfaces read
it — the rail's account row and the Settings CLI card, which the owner requires
to move together (「设置中的这里应该一样的逻辑」). Team-namespaced ids go through
the shared `isTeamPlanTier`, so the badge cannot drift from the other team
gates; `workspaceType === 'team'` covers the free tier that no id can express.
The plan LABEL is untouched, deliberately: it answers a subscription question,
every user-created workspace in B is team-typed, and reading one as the other
is #146. 免费 beside the `team` wordmark is now the intended pairing — the
family is team, the subscription is not paid.
No interaction, layout, sizing, artwork, or copy changes; no new i18n keys.
* fix(collab): warm the workspace-scoped digest faces on switch and reconnect (#6222)
Every workspace-scoped cache in the daemon keys on the active workspace, so the
instant the user switches, `catalog` and `members` are both a miss and the FIRST
consumer in the new workspace refills them inline on its own request path
(`createPersistentSyncCache` fetches synchronously on a miss). The cost is the
same either way; the only question is who waits for it. A switch is a user
action followed by idle time, so paying it there is free — paying it inside the
first project load or agent run is not.
`PUT /api/workspace/active` now announces a CONFIRMED switch through a new
fire-and-forget `onWorkspaceSwitched` seam, and the daemon warms the two faces
from it. The announcement deliberately does not fire for a rejected or
rolled-back switch: warming there would refill the caches against a workspace
this daemon just refused to move to.
Warming goes through the cache functions, never their underlying fetchers.
`createSwrCache`'s read returns `entry.inflight ?? refresh(key)`, so a warm that
races the very consumer it is protecting joins that consumer's request instead
of issuing a second one. No request is added that would not otherwise be made.
Second half, same root cause: on hub reconnect the gap-closing catch-up read the
team-project and member lists THROUGH those same caches, so an entry that
settled just before (or during) the disconnect still counted as fresh, the diff
concluded "nothing changed", and the one cycle meant to close the gap emitted
nothing — the client stayed stale until an unrelated read happened to miss.
Reconnect now drops those two entries first, and `pollOnce` joins the in-flight
refresh that starts, so the pair still costs one fetch per face.
Co-authored-by: lefarcen <ontf116@gmail.com>
* fix(daemon): split AMR workspace proof failure from proof refusal (#6221)
An AMR run died in preflight with "its persisted workspace <id> is
unavailable for the signed-in member". Two causally unrelated situations
shared that one error and one outcome: a directory that answered and
genuinely listed no active membership, and a directory that could not be
read at all. The `.catch` swallowed a read failure into
`{ ok: false, items: [] }`, so a transient vela blip made a user's own
project unrunnable — and nothing downstream could tell the two apart.
Refusal now fails closed under `ProjectWorkspaceScopeRefusedError`
(`PROJECT_WORKSPACE_SCOPE_REFUSED`); an unreadable directory is retried
with bounded backoff and, if it still cannot be reached, the run proceeds
with no workspace, i.e. on the caller's own account wallet. That is a
product decision recorded in the docblock: a hard failure is worse than a
mis-attributed personal charge. The reverse direction, charging a personal
run to a team wallet, stays impossible — no fallback selects another
workspace, and a proven non-membership still refuses.
The decision was also invisible: a full diagnostics export carried no
directory-fetch line, no run-id in the daemon log, and only a generic
AGENT_EXECUTION_FAILED plus prose. Every branch now reports a closed-union
outcome to the daemon log, the run record (via `ApiError.details`, leaving
the wire error code unchanged) and telemetry.
* fix(comments): preserve rejected mutations
* fix(comments): preserve partial send results
* fix(web): keep workspace-scoped data fresh across account and workspace changes (#6225)
* fix(web): keep workspace-scoped data fresh across account and workspace changes
Three defects in one family — workspace-scoped data in the web client going
stale or being read under the wrong identity.
1. The workspace switcher's module-level directory cache had no invalidation.
`/api/workspace/directory` answers "which workspaces can the SIGNED-IN
ACCOUNT see", but nothing cleared the cache on an account change and
`resetWorkspaceDirectoryCache` had no production caller — so signing in as a
different account kept the previous account's workspace names on screen, and
kept them confidently, since a non-empty cache also suppresses the loading
state. The cache is now served only to an identity that can claim it, by
membership id, which keeps the flash-free reopen it exists for. The
`coalescedGet` key is identity-scoped for the same reason.
2. App.tsx read `/api/skills` without the caller's workspace headers and never
refetched on a workspace switch. The daemon is fail-closed on a missing
`x-od-workspace-id`, so a headerless read is not the unfiltered list — it is
the list with every workspace-claimed skill removed, including the ones
claimed by the workspace the user is in. Skills now get the identity headers
and the same workspace-keyed refresh design systems already had. Launch still
spends exactly one `/api/skills` request: the read moved from the boot pass to
the workspace-keyed effect rather than being added alongside it.
3. `PUT /api/workspace/active` already returns the post-switch context, from the
same producer `GET /api/workspace/context` serves and only after the daemon
agrees the switch happened. The client parsed that body, discarded it, and
made every consumer re-read the same data. The shell is now seeded from the
response; the refresh broadcast still fires, because
`useProjectWorkspaceScope` listens to it and passive tabs cannot be seeded.
Switch path: 2 requests -> 1.
* fix(web): discard workspace-scoped reads that land after the identity moved
Review follow-up on #6225 (PerishCode, two threads — both real).
Keying a read by identity stops the wrong identity being SERVED a cached
answer. It does nothing about the other direction: a read issued for identity A
resolves later, and by then the caller may be identity B. Committing that late
answer restores A's data under B — the same staleness this PR removes, arriving
through the back door. A workspace switch is precisely when one read is in
flight and another starts, so reverse-order completion is reachable, not exotic.
Both sites now use one mechanism, `beginWorkspaceScopedRead`: capture the
identity the read is issued FOR, request with that captured context, and compare
against a ref before any commit. Its docblock states the invariant and the two
rules that make it hold (request with the captured context so request and guard
cannot disagree; compare against a ref, never a closed-over prop, which would
always match and guard nothing). It is the cross-component form of the
`requestEpochRef` ordering guard `useWorkspaceContext` already applies to its
own read.
- `refreshSkills` and `handleSkillsChanged` (App.tsx): a slow read for the
workspace the user left no longer overwrites the current catalog. The
registry-ready gate is intentionally skipped on a discarded response — it is
not an answer about the current identity, and the newer read marks it.
- `loadWorkspaceDirectory` (EntryNavRail.tsx): an in-flight account-A read that
lands after the rail moved to account B no longer repopulates either the
module cache or the visible list. This was the sharper of the two, because it
wrote into module scope and so persisted past the render that caused it.
Request counts are unchanged (startup 4, switch 1): the guard discards
responses, it does not issue requests.
* fix(web): make computing a workspace identity total, and key the skills trigger on it
Fixes the `Web workspace tests` failure 886f11b24 introduced. CI reported all
555 files and 5583 tests PASSING and still exited 1:
Unhandled Rejection
TypeError: Cannot read properties of undefined (reading 'canShareProjects')
❯ workspaceIdentityCacheKey src/collab/useWorkspaceContext.ts:112:32
❯ beginWorkspaceScopedRead src/collab/useWorkspaceContext.ts:159:20
❯ src/App.tsx:1770 ❯ src/App.tsx:1799 ❯ commitHookEffectListMount
originating in tests/components/App.amr-plan-tier.test.tsx
`workspaceIdentityCacheKey` dereferenced `context.permissions` unguarded. That
survived while it was only called during render (a throw there fails loudly, in
the right place) and inside `fetchSkills`'s own try/catch. 886f11b24 put it
behind `beginWorkspaceScopedRead` in an async function launched with
`void refreshSkills()`, so a partial context turned a synchronous programming
error into an unhandled rejection — invisible to every assertion, fatal to the
process.
`permissions` is required on the contract, but API JSON reaches these objects
through an unchecked cast, so a partial context is reachable at runtime and not
only in fixtures. Computing an identity must therefore be TOTAL: a partial
context is its own cache partition, never an exception.
Second, independent defect, found while fixing the first: the skills trigger was
keyed on `workspaceId` while the commit guard compares the full identity digest.
A guard that discards without guaranteeing a successor is worse than the
staleness it replaced — two accounts in one shared team workspace differ only by
membership, so account A's pending read would be discarded for B while the
unchanged workspace id suppressed B's replacement read, leaving the functional
registry loading forever. The trigger now uses the same digest the guard
compares, so every transition that invalidates a response also starts its
successor.
Both are covered by tests that go red without the fix:
- tests/collab/workspace-identity-key-is-total.test.ts (new) — identity is total
for null, partial and complete contexts, including a late commit-time guard.
- tests/components/App.skills-workspace-scope.test.tsx — a member-id-only change
must start AND complete a successor read.
---------
Co-authored-by: lefarcen <ontf116@gmail.com>
* feat(landing): publish current pricing snapshot
* fix: durably deliver enterprise leads
* test(web): update ProjectView comment-status suites to the workspace-scoped signature (#6233)
`feat/workspace-team`'s `Web workspace tests` lane is red at the branch tip: 3
files / 4 tests, all comment-status assertions. Not a flake and not attributable
to any open PR — I had to prove twice while landing #6225 that a red lane was
inherited rather than mine, which is the cost a red base imposes on every
downstream PR.
Cause: `1c15574c2 fix(comments): preserve rejected mutations` gave
`patchPreviewCommentStatus` a fifth argument — the acting workspace context — so
the daemon can authorize the mutation (`routes/project/comments.ts`, same
commit). It updated `FileViewer.test.tsx` and added `registry.test.ts` coverage
but left three `ProjectView.*` suites on the four-argument signature.
The tests were stale, not the code. Checked before editing, because "update the
tests" is the wrong move if the assertions were right:
- Every asserted call still happens, unchanged. The failures are pure arity
mismatches: `api-empty-response` expects `(…, 'failed')` and receives
`(…, 'failed', null)`; likewise `needs_review` in run-cleanup and `applying`
in run-isolation. No status transition changed.
- `1c15574c2` shipped the new contract WITH tests —
`describe('preview comment scoped mutations')` asserting `x-od-workspace-id`
reaches the PATCH. The fifth argument is deliberate and covered.
- The parameter is optional and additive; headers are spread only when a
context is present.
Also restores two assertions that had gone silently vacuous. A four-argument
matcher cannot match a five-argument call, so `run-isolation`'s two
`not.toHaveBeenCalledWith` assertions could never match once the signature grew
and passed unconditionally — coverage actively lost, not merely failing. Both
pass with the correct arity, which independently confirms the new code does not
write `needs_review` after a canceled+done run and does not reset a queued
send's own comment to `open`.
The fifth argument is matched as an explicit `null` rather than
`expect.anything()`, which rejects null/undefined and would have re-broken the
positive assertions and re-vacuated the negative ones.
Tests only — no source file is touched.
Co-authored-by: lefarcen <ontf116@gmail.com>
* fix(collab): make the local pin the only workspace-selection truth (#6230)
* test(collab): pin the one-account-two-clients workspace scope behaviour
Executable evidence for the account-level Active Workspace investigation.
Models vela exactly as its source behaves at origin/main 2155a969f:
- active_workspace_selections is keyed by app_user_id alone
(db/schema/public.hcl:792), so an account has exactly ONE selection.
- GET /api/v1/workspaces/current discards a ?workspaceId= hint
(services/api/src/workspaces/routes.ts:166-176, and vela asserts it by
name in services/api/test/workspaces-personal.test.ts:480).
- GET /api/v1/workspaces is scoped by app_user_id, not by the selection.
What it establishes: OD's local pin DOES hold against a competing client
(resolvePinnedWorkspace), so the switch does not 404. The damage is that
the losing client's context degrades to a directory synthesis - planId
null and a 0/0 seat summary, which derives isSeatFull: true - and its
context read gains a second, fallible round-trip the winning client
never makes.
No behaviour change.
* fix(collab): make the local pin the only workspace-selection truth
Picking a workspace is a local action, but `PUT /api/workspace/active`
gated it on mutating ACCOUNT-scoped state in vela: it PUT the account-level
active workspace first and failed the user's click with 502
`workspace_switch_rejected` when that write did not take, then rolled back
both the local pin and the backend switch if the follow-up context read
disagreed.
That backend row is keyed by app user alone, so it can only ever name ONE
workspace for an account. One account routinely runs several clients in
different workspaces, so every switch yanked the other clients' server-side
scope, and whichever client did not write the row last was answered for the
wrong workspace — forcing OD to synthesize its context from the membership
directory, which carries no billing plane. That client saw planId null and a
0/0 seat summary, and a 0/0 summary derives isSeatFull: true, so a workspace
with free seats read as FULL and blocked inviting. It also turned that
client's context read into two round-trips, the second of which could blip
and leave it with no context at all.
vela #1179 (1ba3eebcb, deployed to test 09:57Z) taught
`GET /api/v1/workspaces/current` to honour `x-vela-workspace-id`, the
per-request workspace scope vela already accepted on its resource plane,
billing scope routes and the Link gateway. So:
- `fetchCurrent` sends that header and drops the `?workspaceId=` query
param, which vela ignores by design and asserts it ignores — it only ever
made this call look scoped.
- `PUT /api/workspace/active` no longer calls vela. The membership
directory it already reads is the authorization; once that confirms an
active membership the switch cannot fail, so the 502 gate and the
rollback are gone along with `selectWorkspace` and `putCurrentWorkspace`.
- An unconfirmed context read is no longer treated as a refusal: the route
answers from the directory entry it already validated instead of
reverting the user's choice, mirroring the invariant
`resolvePinnedWorkspace` already follows in the same module.
Verified against the deployed test backend, GETs only: an explicit
workspace returns that workspace (not the account row), a workspace the
caller is not a member of is refused 404 `workspace_member_required`, and
both an absent and a blank header keep the previous account-level
behaviour. The URL query hint is still ignored.
vela's `active_workspace_selections` table and its write path are left
alone; deprecating it is a separate decision and #1179 kept omitting the
header backward compatible.
Two comments that asserted the now-false invariant are corrected.
* fix(collab): never answer a switch with a workspace the pin no longer holds
Review catch (mrcfps): `workspaceContext.current()` is not a passive read.
The vela provider's `resolvePinnedWorkspace` does its OWN fresh directory
lookup and, when that confirms the pinned workspace is gone, clears the pin
and re-pins a recovery workspace so a removed member is not locked out of
everything. The route's directory read can meanwhile have been served from
its 5s cache and still list that workspace.
The previous branch state read `context.workspaceId !== workspaceId` and
treated the provider's recovery as a merely-unconfirmed read, synthesized the
stale cached entry, and returned 200 claiming the requested workspace while
`activeWorkspace.get()` had already become the fallback. That shipped this
PR's own thesis — the local pin is the only selection truth — as a violable
invariant, and the user-visible symptom is the switch appearing to succeed
and then snapping back on the next context poll.
The pin is now re-read AFTER `current()` resolves. If the provider moved it,
the route reports `404 workspace_no_longer_available` for the workspace the
user asked for, never claims it, and does not warm its caches. The recovery
pin is deliberately left in place: reverting it would restore the very
"signed out of every workspace" state it exists to prevent.
The directory entry that authorizes the switch is also now required to be a
LIVE membership (`memberStatus === 'active'`, lifecycle not deleted) — the
same predicate the provider uses. Matching on id alone let a listed-but-
removed membership authorize a switch AND get synthesized into the response.
Tests wire the real provider against the production cached directory fetcher
over one shared pin store, exactly as server.ts does, so the cached and fresh
reads genuinely disagree. Disabling the new pin re-read turns "never claims a
workspace the pin no longer holds" red and nothing else.
---------
Co-authored-by: lefarcen <ontf116@gmail.com>
* fix landing pricing controls
* fix(daemon): resolve the AMR scope-telemetry app version in its own scope (#6240)
Every AMR run in 0.16.2-beta.148 died at spawn with
`spawn failed: appVersionForCapture is not defined`.
`appVersionForCapture` is a local of
`createFinalizedMessageTelemetryReporter`. #6221's
`amr_workspace_scope_resolved` capture called it from `startChatRun`,
~7,500 lines and one function away, so the name resolved to nothing.
The callback runs while building the agent's launch env, inside the
spawn `try`, so the ReferenceError surfaced as the generic
AGENT_EXECUTION_FAILED spawn failure for 100% of AMR runs.
`design.getAppVersion` is the accessor that scope actually has, and it
is the same one the unreachable helper falls back to, so the reported
value is unchanged.
A scope guard covers the class rather than the instance: it runs the
checker over `server.ts` with `@ts-nocheck` stripped and fails on any
identifier that reaches runtime unbound. It flagged one sibling, fixed
here too — the critique orchestrator's `skill` label read
`effectiveSkillId`, a local of `composeDaemonSystemPrompt`. A `typeof`
guard on an undeclared name does not throw, so instead of crashing that
label has silently been `undefined` for every run since #1485.
`@ts-nocheck` on `server.ts` is the enabling condition for both: an
undefined identifier in this file reaches users without any tool
objecting. Removing it is a separate project — 29 daemon files carry it.
* fix(web): isolate workspace identity snapshots
* test(e2e): move the server.ts identifier-scope guard into the merge gate (#6244)
`apps/daemon/tests/server-identifier-scope.test.ts` landed with the
0.16.2-beta.148 AMR spawn fix (#6240) but protected nothing: `ci.yml`'s
"Daemon workspace tests" step runs exactly one file
(`tests/project-watchers.test.ts`), so no other daemon suite executes in
any workflow. The guard sat outside every merge-gate lane.
Widening the daemon lane is not available as a local fix — that exact
command string is pinned by `workflowRunsOnlyAllowedDaemonTest` in
`scripts/check-certain-exempt-consumption.ts`, which conditions the
`trae-cli.test.ts` docs-consumption exception on the lane running nothing
else. Touching it is a coupled scope-policy change.
So host the guard where a lane already runs and the wiring is itself
guarded:
- Any change under `apps/daemon/src/` matches `certain-daemon-core`
(`scripts/scopes.ts`), whose effects include
`ui_p0_validation_required`; `run_e2e_vitest` is
`isFull || web_tests_required || ui_p0_validation_required`. A
`server.ts`-only change therefore arms `E2E Vitest` even at the merge
queue's `certain` threshold, and an unresolved file list escalates
fail-closed to full.
- The `daemon core boundary` guard (`scripts/lib/guard/scope.ts`) asserts
`ci.yml` still contains both `run_e2e_vitest == 'true'` and
`pnpm --filter @open-design/e2e test`, and runs in the always-on policy
floor — so the premise cannot rot silently.
The check only ever reads `server.ts` as text (no daemon boot, no
fixtures), and reading another app's tree as a repository resource is
what root `AGENTS.md` sends to `e2e/tests/` — the same reason
`e2e/tests/critique-coverage.test.ts` lives there. Behavior is
unchanged, including both self-checks that keep the guard from going
vacuous.
Verified red on the pre-#6240 `server.ts`: exit 1 naming
`appVersionForCapture (line 9261)` and `effectiveSkillId (lines
9657/9658)`; green (exit 0) on the fixed file.
* fix(collab): align unbound project mutation identity (#6231)
* fix(web): name the caller when a run starts before its project scope resolves
Sending the 「水面焦散」 example prompt from Home failed instantly on a team
workspace:
daemon 401: {"error":{"code":"WORKSPACE_CONTEXT_REQUIRED","message":"workspace context is required"}}
with `run_id: n/a`, because the refusal lands before the run is created. That
string is produced in exactly one place in the repo — the `!createResp.ok`
branch of the `POST /api/runs` create fetch in `providers/daemon.ts` — so the
refused call is run creation, gated by `enforceWorkspaceResourceMutation
('project', …)`.
The cause is client-side. Home creates the project WITH the caller's workspace
headers, so #6201 binds it to the team workspace; the follow-on auto-send then
went out with NO headers, because it took its identity from the PROJECT's
workspace scope — an async read still in flight when the auto-send fires (its
gate is `messagesInitialized` + `activeConversationId`, never the scope). Every
other project write in `ProjectView` (`patchProject`, `uploadProjectFiles`, the
comment mutations) already asserts the caller's own context; run creation was
the one write that did not.
INVARIANT, now named in `runWorkspaceIdentity`: a send names its caller while
the project's binding is still UNREAD, and never overrides an answer the daemon
actually gave. Deliberately not a blanket `?? caller` — every answered or failed
state keeps asserting nothing, because for them a headerless request is judged
on daemon-owned state while an asserted one is judged on client-controlled
headers:
- `unbound` — `headerlessMutationAllowed` short-circuits on "no row anywhere"
BEFORE asking for an identity, so asserting would turn a working 200 into a
403 and break 「未登录也可以用自己 cli 修改未登录态下的那些 project」.
- `unavailable` — the daemon's `resolveProjectWorkspaceScope` returns this
both for an unreadable directory AND for a directory that answered with no
active membership, i.e. a proven "you were removed".
- a FAILED scope read — this identity also picks the wallet the AMR pre-run
balance gate prices against, and guessing the caller's team wallet with the
binding unknown blocks a send the daemon would have answered (the dead
button 21f452ffe undid).
Second consequence of the same null, fixed by the same one-liner: with no
resolved scope, `checkAmrBalanceGate` was called with `undefined` and priced the
run against the ACCOUNT wallet instead of the team's. The red spec observes
that directly rather than arguing it.
`ProjectView.run-isolation.test.tsx` mocks `useProjectWorkspaceScope` with a
hand-written factory that enumerates every export, so the new export is added
there too — without it, adding one export breaks all 64 tests in that file with
"not a function" at render.
* fix(web): narrow the run-identity fallback to a project's first unread binding
Review catch (nettee): keying the fallback on `loading` served two opposite
situations, because `useProjectWorkspaceScope`'s trailing transition guard
re-enters `{ loading: true, scope: null }` whenever the resolved caller identity
stops matching — and every workspace switch does that:
- first read, nothing known -> the caller is the only identity available
- revalidating a KNOWN binding -> the caller's current workspace is a guess
that contradicts an answer we already have
So a send issued during a workspace switch asserted the workspace the user had
just moved TO for a project still bound to the one they moved FROM. Both
consumers break, and differently: `POST /api/runs` can 403 because
`enforceWorkspaceResourceMutation` still finds the project row under the old
workspace, and `checkAmrBalanceGate` preflights the new workspace's wallet —
worse than the `undefined` this fix was written to remove, because a wrong
wallet looks like a real answer.
`ProjectWorkspaceScopeState` now carries `initialLoadPending`, reported by the
hook rather than inferred from `loading`: true from mount until the first settled
answer (a scope OR a failure) for the current `projectId`, false for that
project's whole lifetime afterwards, and reset when `projectId` changes because a
different project genuinely has not been read. `runWorkspaceIdentity` keys the
fallback on that bit alone, and its docblock names which of the two loading
reasons it serves so the distinction cannot erode.
The field is required, not optional, so every construction site has to state
which case it is. Two pre-existing assertions in
`useProjectWorkspaceScope.test.tsx` now document the distinction they always
exercised: a project A->B rerender reports pending (B is unread), while a
same-project identity change reports not-pending — the exact window this fixes.
* fix(collab): align unbound project mutation identity
Allow an identified caller to mutate a project that no workspace has claimed, matching the existing headerless and unbound-resource behavior without persisting a new binding. Name the caller on Home auto-send while keeping AMR preflight scope derivation unchanged.
* fix(web): refresh the settled run billing scope
Track the project billing context in both callbacks that read it so an auto-send after scope settlement cannot preflight through a stale unscoped closure. Add a regression whose run identity stays referentially stable while the billing scope changes.
* fix(web): stop asserting disproved run scope
* fix(web): guard workspace-scoped catalog reads (#6249)
* fix(web): guard workspace-scoped catalog reads
* fix(web): guard remaining plugin catalog races
* fix(web): align design catalog guard scope
* fix(landing): show introductory team totals
* fix(daemon): label critique with project skill (#6251)
* fix CI scopes across file renames (#6252)
* fix(ci): preserve scopes across renames
* fix merge queue rename ceiling detection
* test raw merge compare responses
* fix(landing): make contact sales KV canonical
* fix(web): preserve Cloud sign-in during outages (#6274)
* fix(collab): allow shared project viewer comments
* fix(web): keep readonly comment creation available
* feat(web): personal/team project labels + collapse chat for shared non-owners (#6294)
* feat(web): rename sidebar project lists and collapse chat for shared non-owners
Workspace home left rail now labels personal vs team project lists
("Personal projects" / "Team projects", 个人项目 / 团队项目) across all
locales. Opening a confirmed shared team project that the viewer did not
create defaults the chat pane to collapsed so the design is primary;
owners and personal projects keep chat open, and expanding chat stays
sticky for the visit.
* fix(web): use effective ownership for shared non-owner chat default
Default-collapsed chat was latching on raw isOwner, so a catalog-confirmed
owner whose collab status was still missing ownerMemberId looked like a
non-owner and permanently collapsed chat. Expose isEffectiveOwner and
isSharedNonOwner from useProjectCollab (catalog + session-created + status)
and gate the sticky collapse path on the confirmed non-owner signal only.
* fix(web): stop narrow chat error cards stacking titles vertically (#6295)
Move the long Open Design Cloud switch CTA into UserActionCard footer
actions (same shell as run-recovery), add a card-width container query so
head actions reflow when ChatPane is narrow, and soften title wrapping so
CJK copy no longer renders one character per line.
* fix(workspace): make resource authority explicit (#6287)
* fix: make workspace authority explicit across resources
* fix(web): keep new team projects writable during scope load
* fix: close workspace authority review gaps
* fix(web): retry rejected project auto-send
* fix: propagate workspace scope to project consumers
* fix: cache project read workspace authority
* fix(web): stabilize project scope revalidation
* fix: cache project collaboration status authority
* fix: cache collaboration status owner reads
* fix: revalidate media wait tool authority
* fix: rehome member comments on local threads
* fix(web): stabilize project resource authorization
* fix(web): prioritize settled project authority
* fix(web): consume queued home auto-send handoffs
* fix: revalidate dirty comment pulls
* fix: isolate mutation authority reads by session
* fix: cache project presence read hot path
* fix(web): stop unauthorized project background writes
* fix(web): require settled scope for project background work
* fix(web): scope remaining project reads by workspace
* fix(web): scope remaining project resource consumers
* fix(web): partition project caches by full authority
* fix(daemon): pin run workspace billing scope
* fix(daemon): lease workspace read authority
* fix(daemon): keep presence reads on the hot path
* fix(web): refresh deck thumbnails across workspace scope
* fix(daemon): revoke stale unshared team mirrors
* perf(daemon): cache stale mirror read guards
* fix(web): gate deep-link reads on project workspace
* fix(web): keep deep-linked project tab scoped
* fix(web): preserve project routes during onboarding
* fix(web): keep project previews warm across file-list toggles
* perf(daemon): refresh presence without cold reads
* fix(web): keep retained preview iframes connected
* fix(web): reuse exact HTML thumbnail sources
* fix(web): settle read-only empty project chat
* fix(daemon): scope CLI run transports by workspace
* test(web): align preview cache refresh semantics
* fix(web): require positive shared viewer ownership proof
* fix preview auto-fit measurement feedback
* preserve preview fit during annotation freezes
* preserve preview measurement semantics on refresh
* stabilize preview overflow measurement test
* test design systems move exclusively to team
* fix design systems move exclusively to team
* test shared project title scope convergence
* test(daemon): cover project command workspace scope
* fix(daemon): scope project command transports
* fix shared title test project state type
* fix(daemon): preserve shared project catalog title
* fix(daemon): keep foreign mirrors creator-unattributed
* fix(web): recover first shared project conversations
* fix(web): fence conversation recovery generations
* test(web): verify conversation recovery cleanup fence
* UI polish batch: cloud sign-in, onboarding, composer menu (#6281)
* UI polish batch: cloud sign-in, onboarding, composer menu
Sign-in surfaces (rail callout, balance-gate dialog, onboarding) had
drifted from the visual system and lost some redundant chrome; this
batch tightens them up alongside a couple of composer/design-files
reorganizations that were sitting in the same working tree:
- Rail's signed-out callout: reorder copy above the login pill, give
the pill an icon + accent-green label, restore the Escape hatch
the pill had before it lost its close affordance.
- AmrBalanceDialog (insufficient-balance / signed-out gate): add a
full-bleed art banner above the copy, drop the now-redundant icon
badge, tighten spacing/shadows to match.
- Onboarding cloud landing: switch from a single centered column to
a two-column layout (form pane + full-bleed art panel), move the
language switcher into the footer, swap the local-CLI/BYOK links
for icon Buttons, and add a dismiss control to the activation-retry
hint.
- Composer "+" menu: fold 插件/设计百宝箱 back in as hover-expand
submenu rows instead of standalone quick pills above the input.
- Design files panel: move new-document/upload actions out of the
always-visible toolbar into the empty-state actions only.
- Message center: drop the redundant header close button (backdrop
click and Escape still dismiss it) and restyle the list from
bordered cards to a divider-separated list.
- New `key` / `robot` / `translate` / `arrow-right` icons; language
menu switches from the `languages` glyph to `translate`.
- Tab launcher popover reverts to the glass material; language
dropdown gets a max-height + scroll so long lists don't blow out
the popover.
Rebased the composer-quick-pill removal onto this branch's own
accessibility work on those pills (opener-based focus return via
`ComposerStandalonePanel`) rather than reverting it — the "next step"
card still drives the same standalone-panel API, just without a pill
to hand focus back to. Also updated/removed the tests that pinned the
now-removed surfaces (quick-pill popup contract, message-center close
button, onboarding top bar, design-files toolbar upload trigger) so
none of this lands with a newly-red suite.
* perf(web): optimize onboarding cloud artwork
* fix(web): invalidate project file reads after mutations
* fix(web): disable readonly project starter mutations
* fix(web): preserve focus for standalone composer panels
* test(e2e): require explicit workspace scope
* fix(web): invalidate file reads on project events
* test(e2e): seed explicit settings workspace
* test(e2e): align manual edit with current inspector
* fix(web): refresh deployments when share opens
* test(e2e): follow composer design system entry
* fix(web): keep local project creation workspace-independent
* fix(web): preserve scoped auth recovery state
* test(e2e): follow current entry settings flows
* fix(web): preserve AMR retry across settings
* test(e2e): model explicit personal workspace recovery
* test(e2e): stabilize AMR logout settings recovery
* fix(projects): return workspace scope on create
---------
Co-authored-by: lefarcen <935902669@qq.com>
* fix(workspace): preserve scoped projects after main sync (#6320)
* fix(daemon): surface AMR OpenCode stall context (#6040)
* fix(daemon): surface AMR OpenCode stall context
* fix(daemon): accept runtime-omitted OpenCode diagnostics
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* fix(ci): make static gate runner-independent (#6039)
---------
Co-authored-by: PerishFire <39043006+PerishCode@users.noreply.github.com>
* test(e2e): parallelize restoration group (#6169)
* test(e2e): parallelize restoration group
* test(e2e): identify conversations from persisted prompts
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* test(e2e): wait for conversation persistence
---------
Co-authored-by: Looper <looper@noreply.github.com>
* feat(landing-page): sharpen the Codex agent page for "codex ui" intent (#6200)
The /agents/codex-design/ page ranks #1 for "codex design" but only ~6th
with a 1.3% CTR for "codex ui", a query that grew from 30 to ~90 daily
impressions this month. A live SERP check shows the intent is mostly
"build UI with Codex" (our intent), with a minority looking for the Codex
app's own interface.
- Retitle to "Codex Design: Build UI with OpenAI Codex" (title + H1).
- Lead the frontend section with the query phrase and an answer-shaped
opening paragraph, so the section is extractable for AI answers.
- Add a disambiguation paragraph separating the Codex app's own UI from
the UI you build with Codex, and route the former back to the main line.
- Add two FAQ entries covering both readings; they also feed the FAQPage
schema, which already earns a text snippet on this query.
Applied across all 11 shipped locales (en, zh authored; the other nine
translated with the page's terminology preserved).
Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
* Add Clone Audit plugin (#5687)
* Add Clone Audit plugin
* Update open-design.json
* fix: make clone audit skill reusable
* fix: sanitize clone audit provenance path
* fix(e2e): bound tools-dev runtime lifecycle (#6204)
* test(e2e): refresh full-pool UI fixtures (#6211)
* fix(web): allow skipping required chat questions (#6177)
* fix(web): allow skipping required chat questions
* fix(web): auto-skip unanswered chat questions
---------
Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
* docs(tools-pack): add build cache contract (#6207)
* docs(tools-pack): add build cache contract
Document the tools-pack build-graph cache as a contract: the exact-match
acceptance model, determinant rules, materialization-time parameters, the
signing boundary, and fail-closed confidence grading.
The rules come from an audit of all 11 cache nodes. Two are written down
because nothing currently enforces them: a node key must carry the key of
every upstream node it consumes, and a node key must not restate a list that
already exists as a constant.
Known low-confidence points are declared explicitly so fail-closed grading
stays checkable rather than becoming a verbal convention.
* docs(tools-pack): clarify cache materialization exceptions
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* docs(tools-pack): document NSIS base version scope
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
---------
Co-authored-by: Looper <looper@noreply.github.com>
* docs(landing-page): add 0.16.0 release post, localize and re-cover 0.15.1 (#6236)
* docs(landing-page): localize 0.15.1 post, fix its cover, add 0.16.0 release post
The 0.15.1 blog post shipped English-only and with an off-brand cover: a
Fauvist oil painting overlaid with large display type, which reads nothing
like the warm editorial illustrations every other release post uses. It also
pointed `socialImage` at a 309KB JPEG while a WebP sat unused beside it.
Regenerate the cover in the established house style (cream paper ground,
sage/terracotta watercolour washes, pencil sketch resolving into a calm
product surface), drop the stray JPEG, and add the full ten-locale i18n block
so /zh/, /ja/, /ko/, /de/, /fr/, /ru/, /es/, /pt-br/, /it/ and /tr/ read in
their own language instead of falling back to English.
Also add the missing 0.16.0 "Reliable Delivery" post with a cover plus two
inline figures in the same style, localized across the same ten locales. The
0.16.1 patch is a single preview fix, too thin to carry its own page, so it
lands as a closing section of the 0.16.0 post and the download CTA points
there.
* docs(landing-page): fix doubled apostrophes and locale copy in release posts
Three defects surfaced reviewing the localized bodies.
The French, Italian and Turkish bodies rendered every apostrophe twice —
"aujourd''hui", "all''interno", "0.16.1''i". The `''` escape belongs to
single-quoted YAML scalars, but `bodyHtml` is a block scalar, where content is
literal. 173 occurrences across the two posts.
Korean used Japanese corner brackets and Brazilian Portuguese and Turkish used
French guillemets. All three take curly double quotes.
The Chinese copy read as translation rather than prose. "标签" opens the release
line as a UI label, not a version tag; "抢画框", "编辑策展位" and "安静修复" are
literal renderings that say nothing in Chinese; "画廊" is not the term the
product itself uses for a gallery, which keeps the English word. The tag fix
also lands on 0.10.0 through 0.15.0, which share the phrasing — leaving the
series half-corrected would read worse than either state alone.
* docs(landing-page): drop the leading tag word from release post openings
Every release post opened with the word for a git tag before the version
identifier — "Tag open-design-v0.16.0", "Etiqueta …", "タグ …", and in Chinese
"标签", which reads as a UI label rather than a version. The word carries no
information the identifier does not already carry, and in several languages it
lands as a category noun the sentence never uses again.
Drop it across all nine release posts and every locale, in both the summary and
the opening line, leaving the version identifier to open the dateline. Spanish
and Brazilian Portuguese lose the feminine agreement that referred back to the
removed noun; Turkish loses the trailing "etiketi" apposition for the same
reason.
* docs(landing-page): correct the 0.16.1 interval in the 0.16.0 post
0.16.0 was published 2026-07-22 14:42 UTC and 0.16.1 on 2026-07-23 16:11 UTC —
25.5 hours apart. "Two days later" overstated it, and the same wording had been
carried into all ten localized bodies, so every published route repeated it.
---------
Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
* feat(integrations): add the local Open Design Cloud flow for Codex (#6055)
* feat(daemon): expose Vela login through CLI and MCP
* feat(mcp): add interactive local brief card
* feat(packaged): bootstrap local MCP headlessly
* feat(daemon): secure local BYOK credential profiles
* feat(cli): manage secure BYOK profiles
* feat(web): store BYOK keys in secure profiles
* fix(runs): deduplicate cloud generation requests
* feat(daemon): add end-to-end plugin observability
* fix(pack): include native PTY runtime in desktop packages
Lazy-load node-pty in the daemon so missing native binaries only
disable Terminal and interactive Keychain operations instead of
crashing startup.
Ship and validate target prebuilds in macOS and Windows packages.
Repair the macOS spawn helper before signing, and invalidate stale
Windows packaging caches.
Cover the packaged PTY path in macOS and Windows smoke tests. Require
Vela CLI for every beta desktop build target.
* fix(mcp): localize and stabilize plugin briefs
Keep collect and confirm on one MCP Apps card, report intrinsic size changes,
and remove the fragile widget-state fallback.
Localize brief forms, summaries, status, and public errors from the current
request or Host locale while preserving stable protocol ids and selections.
Keep internal runtime identifiers out of user-facing MCP guidance.
* fix(packaged): register live Studio URLs with daemon
Register the packaged web sidecar loopback origin over namespace-scoped daemon
IPC after dynamic binding.
Invalidate install-info by the live web port and remove the second MCP URL cache
so run and project responses use the current Studio address.
Validate loopback origins and cover registration, rebind, and cache refresh.
* fix(mcp): stabilize brief confirmation lifecycle
Keep confirmed briefs locked after Host publication failures and retry only the cached follow-up message instead of confirming twice.
Replace blank initialization with a compact loading state, accept immediate and delayed tool results, and bump the MCP Apps resource to v3 so Codex does not reuse the stale card.
Add an executable JSDOM Host bridge harness covering double-submit, publication retry, delayed results, and repeated draft notifications. Validated with the full daemon suite, workspace typecheck, and guard.
* fix(mcp): prevent brief app resize feedback loops
Replace the self-observed intrinsic-height loop with deduplicated body measurements so Codex MCP app iframes do not repeatedly resize themselves and fail sandbox startup.
Bump the brief resource URI to v4 to avoid cached v3 cards and cover the regression with the focused brief-app spec. Validated with daemon tests/build, workspace guard/typecheck, a verified beta DMG, and a direct packaged MCP resource smoke.
* fix(mcp): use the final Open Design plugin identity
Make open-design the only accepted external plugin ID across MCP tools, run attribution, artifact version origins, and telemetry. Rename the Brief MCP App resource to v5 and reject the unpublished old ID instead of aliasing it.
Validated with 171 daemon tests, 5 web tests, daemon/web typechecks, and guard.
* chore(pack): bump Vela CLI to 0.0.27
* chore(nix): refresh pnpm dependency hashes
* fix(mcp): honor host and plugin request contracts
Respect independently advertised MCP Apps capabilities and prefer the Codex-native follow-up bridge so confirmed briefs render as readable user turns instead of context-only messages. Bump the brief resource to v6 to invalidate stale cards.\n\nMark Vela login and account probes as open-world interactions, and require stable request IDs for attributed generation so lost responses cannot create a second logical run.
* fix(cli): support keyless BYOK profiles
Add an explicit --no-api-key save mode for local and self-hosted providers while keeping credential-bearing profiles stdin-only. Reject missing or conflicting credential modes and cover both paths in the CLI tests.
* fix(pack): replace headless owner when desktop opens
Advertise headless packaged runtimes as having no visible window, then shut them down before a normal desktop launch continues instead of treating SHOW as a successful focus.
Validated with the focused packaged headless and launcher lifecycle tests (20 passed).
* fix(pack): clean up failed headless startup
Acquire the headless identity, sidecars, and IPC server through one failure-atomic lifecycle so later bootstrap errors close every previously acquired owner in reverse order.
Cover MCP-install and web-identity publication failures; validated with 22 focused tests and the packaged typecheck.
* fix(byok): remove raw provider request contract
Expose only daemon-owned BYOK profile references from shared run types and the web transport, while retaining the daemon's fail-closed rejection for legacy or untyped raw credential payloads.
Validated with 76 web provider tests plus web and contracts typechecks.
* fix(daemon): preserve interrupted run idempotency
Hydrate queued or running durable requests as DAEMON_RESTARTED failures before idempotency reuse, preserving the original request and workflow mappings instead of creating a second cloud operation.
Share the restart transition with boot reconciliation and expose synthetic terminal events to reattaching clients. Validated with 48 focused tests and the daemon typecheck.
* fix(byok): roll back failed credential metadata writes
Snapshot secure-store values before mutation and compensate create, update, and delete operations when profile metadata cannot be persisted, preventing orphaned or lost credentials.
Validated with nine focused credential-service tests and the daemon typecheck.
* fix(web): reconcile selected BYOK profile metadata
Treat the daemon's selected secure profile as authoritative for protocol, endpoint, model, and API version so CLI profile edits cannot leave the browser showing stale execution settings.
Validated with 66 config tests and the web typecheck.
* fix(byok): migrate legacy credentials before cleanup
Import each supported legacy browser credential into a stable daemon-owned secure profile before removing plaintext copies. Keep the original browser record on secure-store or unsupported-provider failures and surface a Settings recovery path.
Cover successful migration, duplicate projections, and injected secure-backend failure while preserving existing Bedrock downgrade behavior in memory.
* fix(byok): secure Windows profiles with DPAPI
Dispatch native Windows BYOK profiles to a CurrentUser DPAPI backend rooted under the resolved daemon data directory. Secrets enter PowerShell only through stdin, encrypted blobs stay separate from non-secret profile metadata, and API responses remain secret-free.
Add platform dispatch coverage, a Windows-only real DPAPI round-trip test, and the packaged Windows smoke path for create, resolve, response-redaction, and delete behavior.
* fix(web): preserve config writes during hydration
* fix(byok): persist secure credentials across entry flows
* test(byok): cover secure onboarding persistence
---------
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: lefarcen <935902669@qq.com>
* test: strengthen E2E coverage and fixtures (#6150)
* test: strengthen e2e coverage and fixtures
* test: allow artifact route when starting conversation
* test: keep e2e count assertions stable
Use the shared stable-count helper for no-request and duplicate-event windows so delayed async events cannot escape the assertions.
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* fix: add e2e assertion import extension
Use the explicit runtime extension required by the e2e NodeNext typecheck.\n\nGenerated-By: looper 0.11.0 (runner=fixer, agent=codex)
* fix: preserve design system provenance and settle e2e assertions
Keep disabled design systems available to project recovery and provenance while filtering only runtime injection. Make hover and cancellation checks observe their full settled windows.
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* test(e2e): preserve unique restoration run IDs
Pass the restoration fixture's run ID prefix through routeSuccessfulRuns so each mocked request receives a distinct run identity.
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* test(e2e): stabilize agent and async count fixtures
Route restoration agent requests through the shared JSON and SSE fixture, capture autosave baselines after a quiet period, and observe onboarding cancellation for a bounded stable window.\n\nGenerated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix: honor disabled design systems in daemon runs
Filter disabled persisted project selections at prompt and analytics boundaries, and keep restoration run fixtures unique.\n\nGenerated-By: looper 0.11.2 (runner=fixer, agent=codex)
* test(e2e): shorten cancellation duplicate check
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(daemon): continue stalled post-tool sessions (#6237)
* fix(daemon): continue stalled post-tool sessions
* fix(daemon): separate post-tool continuation budget
Generated-By: looper 0.11.1 (runner=fixer, agent=codex)
* fix(daemon): preserve retry telemetry budget
Generated-By: looper 0.11.1 (runner=fixer, agent=codex)
* fix(daemon): align retry failure telemetry
Generated-By: looper 0.11.1 (runner=fixer, agent=codex)
* fix(tools-pack): carry workspace build key into tarballs (#6235)
* fix(amr): recover late login failures and trace auth stages (#5986)
* fix(amr): recover late login failures and trace auth stages
* fix(amr): preserve scoped cancel attempt IDs
Generated-By: looper 0.11.1 (runner=fixer, agent=codex)
* fix(amr): defer unsupported Vela auth stages
Generated-By: looper 0.11.1 (runner=fixer, agent=codex)
* fix(amr): narrow fallback telemetry to shipped Vela
Generated-By: looper 0.11.1 (runner=fixer, agent=codex)
* fix(amr): preserve live login after stale cancel
Generated-By: looper 0.11.1 (runner=fixer, agent=codex)
* fix(amr): preserve cancel during login startup
Retain cancellation intent when a provisional cancel races the delayed canonical login response, then cancel the canonical attempt before polling can begin.
Generated-By: looper 0.11.1 (runner=fixer, agent=codex)
* fix(amr): rejoin newer login after startup cancel
Generated-By: looper 0.11.1 (runner=fixer, agent=codex)
* fix(amr): preserve cancel across web login starts
Generated-By: looper 0.11.1 (runner=fixer, agent=codex)
* fix(amr): retain cancel when status refresh fails
Generated-By: looper 0.11.1 (runner=fixer, agent=codex)
* fix(amr): cancel onboarding status preflight
Treat cancellation before an auth attempt exists as a local preflight cancel so a delayed status response cannot start login.\n\nGenerated-By: looper 0.11.1 (runner=fixer, agent=codex)
* ci: constrain Actions cache writes (#6270)
* fix(tools-pack): carry workspace build key into tarballs
* ci: constrain Actions cache writes
* fix(daemon): auto-mint assistantMessageId when clients omit pin (#6107)
* fix(daemon): auto-mint assistantMessageId when API clients omit it
Web always supplies assistantMessageId. Headless/MCP/eval clients that
already send conversationId but omit the pin left lastMessageId null and
skipped multi-turn native session resume (missing_cursor). Mint the id
server-side whenever a conversation is bound and the client did not, and
seed the user message the same way the old MCP-only path did.
* fix(daemon): validate conversation ownership before omit-pin seed
Reorder project-ownership checks before minting assistantMessageId and
seeding the user prompt so cross-project omit-pin requests cannot corrupt
foreign chats. Make ChatRunCreateRequest.assistantMessageId optional and
expose conversationId/assistantMessageId on McpRunCreateRequest with
contract tests for the omit-pin shape.
Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)
* fix(daemon): reject missing conversation before omit-pin mint
Return CONVERSATION_NOT_FOUND when a client supplies a stale or mistyped
conversationId so omit-pin does not mint an unpersisted assistantMessageId
and plugin snapshot resolve does not FK-crash with a 500.
Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)
* fix(daemon): seed latest turn and fallback-bound user prompts
Prefer currentPrompt over full message transcripts when seeding omit-pin
user turns, and still seed after conversationId fallback when the client
already supplied assistantMessageId.
Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)
* fix(daemon): preserve attachments on omit-pin seeded user turns
Copy request attachments and commentAttachments into the server-seeded
user message so omit-pin/headless clients keep chips and annotation
context after conversation reload.
Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)
* fix(daemon): treat empty currentPrompt as authoritative omit-pin seed
Attachments-only ChatRequest turns send currentPrompt as '' while message
is the flattened transcript. Prefer any string currentPrompt, including
empty, so seeded user rows keep empty content and do not duplicate history.
Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)
* fix(daemon): seed turn metadata and bump project activity on omit-pin
Persist sessionMode, runContext, and appliedPluginSnapshot on omit-pin
seeded user turns so reload/retry keep applied context. Call updateProject
after seed so listProjects reorders headless/API activity like PUT /messages.
Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)
* fix(daemon): require projectId for bound conversation ownership
Reject POST /api/runs when conversationId is present but projectId is
missing or non-string, so omit-pin cannot seed messages without owning
project context. Cover both early and post-fallback ownership checks.
Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)
* fix(daemon): preserve slideIndex and BMP kind on omit-pin seeds
Keep deck annotation slideIndex and classify .bmp as image when seeding
user messages for clients that omit assistantMessageId.
Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)
* fix(daemon): seed empty message when omit-pin has attachments
When currentPrompt is unset and message is empty, still seed the user
turn if attachment metadata is present so chips/annotations survive reload.
Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)
* fix(daemon): seed omit-pin chat content from original request
Plugin resolution may rewrite meta.message with a rendered scenario
brief for the run; persist requestBody currentPrompt/message for the
user turn so the internal brief is not shown as chat content.
Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)
* fix(web): persist browser preview viewport (#4899)
* fix(daemon): honor Kiro ACP turn completion (#6268)
* fix(daemon): isolate plugin-started Local Codex runs (#6273)
Disable Codex plugins only for externally attributed Open Design Plugin runs so Local Codex cannot recursively enter collect_brief or Cloud login. Preserve normal Local Codex plugin loading and the existing operator-wide override.
Validated with the focused runtime args suite, workspace typecheck, guard, and an isolated source smoke that produced a valid artifact without Cloud login.
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
* fix(landing): accept signed Vela team plan leads (#6283)
* fix: durably deliver enterprise leads
* fix(landing): make contact sales KV canonical
* feat(prompts): introduce on-demand discovery in SP v2.0 (#6223)
* feat(prompts): introduce on-demand discovery in SP v2.0
* fix(prompts): prevent nested deck label overlap
* fix(plugins): stabilize od-default task routing
* fix: address prompt and deck audit regressions
* fix(prompts): restore plan mode precedence
* fix(daemon): accept all form answer headers
---------
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
* fix(web): surface BYOK migration validation errors (#6280)
* fix(web): surface BYOK migration validation errors
Preserve structured daemon errors during legacy credential migration and reserve offline guidance for fetch failures.
Keep legacy browser credentials until migration succeeds, and cover HTTP validation, malformed responses, and network failures.
Validated with focused web tests, web typecheck, workspace guard, and workspace typecheck.
* fix(web): stabilize BYOK persistence error telemetry
Map secure-profile HTTP and network failures to semantic error codes before emitting settings_byok_test_result, while preserving existing fallback behavior for unrelated connection-test errors.
Validated with the complete web test suite, web typecheck, workspace typecheck, guard, and git diff --check.
---------
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
* fix(ui): improve dark-mode contrast for off-state toggles (#5825)
* fix(ui): improve dark-mode contrast for off-state toggles
The default off-state track uses --border-strong (#c9d0da light / #46433c dark).
In dark mode #46433c is only ~5% lighter than the --bg-subtle (#252321) panel
behind it, making the toggle nearly invisible when off and users unsure of the
control state.
Bump the off-state track to #6b6862 with a subtle inset border, and add a
thin outline to the white thumb so it stays visible against the (now lighter)
track. On-state already uses --text (#e8e4dc) and is left unchanged.
Applies to both .toggle-row (used by speaker-notes toggle in the deck tab)
and .compact-toggle (used by SurfaceOptions toggles). Hover lift gives an
additional interactive affordance.
Fixes#5298
* fix(ui): fix two dark-mode toggle blocking issues from review
Two correctness issues called out by mrcfps:
1. System dark theme never gets the contrast fix — the default "system"
theme mode removes [data-theme] and falls back to
@media prefers-color-scheme. Added a parallel block scoped to
html:not([data-theme]) inside @media (prefers-color-scheme: dark).
2. Hover override wins over on-state — .compact-toggle:hover and
.toggle-row:hover have higher specificity than .on
variants, so hovering an ON toggle dropped its track from bright
var(--text) to #75726c. Changed to :not(.on):hover so the hover
lift only applies when off, leaving the on-state always bright.
Both applied to both selector paths (explicit data-theme and system dark).
* fix(web): keep Azure deployment names editable (#6034)
* chore(plugin-previews): refresh baked preview manifest (#6293)
Co-authored-by: open-design-bot <bot@open-design.ai>
* fix(byok): withdraw Windows DPAPI backend (#6308)
* fix(byok): withdraw Windows DPAPI backend
* fix(byok): clean up retired Windows secret blobs
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
---------
Co-authored-by: Looper <looper@noreply.github.com>
* fix(web): dismiss stale todo after continuation starts (#6307)
* fix(web): preserve shared project update timestamps
* fix(web): reconcile shared timestamps on home
* fix(web): preserve workspace scope in preview assets
* fix(collab): avoid unscoped shared cover requests
* fix(web): avoid double-wrapping preview assets
* fix(web): preserve first prompt across project refresh
* test(web): target the active shared project card
* test(web): keep empty BYOK profile type-safe
* test(e2e): require explicit workspace scope
* fix(web): finish scoped preview hydration
* test(e2e): align manual edit with current inspector
* fix(web): reconcile files after event stream readiness
* fix(web): keep local project creation workspace-independent
* fix(web): preserve scoped auth recovery state
* test(e2e): assert explicit Personal workspace scope
* test(e2e): preserve visual AMR account fixture
* test(web): await scoped cover source
* UI polish batch: cloud sign-in, onboarding, composer menu (#6281)
* UI polish batch: cloud sign-in, onboarding, composer menu
Sign-in surfaces (rail callout, balance-gate dialog, onboarding) had
drifted from the visual system and lost some redundant chrome; this
batch tightens them up alongside a couple of composer/design-files
reorganizations that were sitting in the same working tree:
- Rail's signed-out callout: reorder copy above the login pill, give
the pill an icon + accent-green label, restore the Escape hatch
the pill had before it lost its close affordance.
- AmrBalanceDialog (insufficient-balance / signed-out gate): add a
full-bleed art banner above the copy, drop the now-redundant icon
badge, tighten spacing/shadows to match.
- Onboarding cloud landing: switch from a single centered column to
a two-column layout (form pane + full-bleed art panel), move the
language switcher into the footer, swap the local-CLI/BYOK links
for icon Buttons, and add a dismiss control to the activation-retry
hint.
- Composer "+" menu: fold 插件/设计百宝箱 back in as hover-expand
submenu rows instead of standalone quick pills above the input.
- Design files panel: move new-document/upload actions out of the
always-visible toolbar into the empty-state actions only.
- Message center: drop the redundant header close button (backdrop
click and Escape still dismiss it) and restyle the list from
bordered cards to a divider-separated list.
- New `key` / `robot` / `translate` / `arrow-right` icons; language
menu switches from the `languages` glyph to `translate`.
- Tab launcher popover reverts to the glass material; language
dropdown gets a max-height + scroll so long lists don't blow out
the popover.
Rebased the composer-quick-pill removal onto this branch's own
accessibility work on those pills (opener-based focus return via
`ComposerStandalonePanel`) rather than reverting it — the "next step"
card still drives the same standalone-panel API, just without a pill
to hand focus back to. Also updated/removed the tests that pinned the
now-removed surfaces (quick-pill popup contract, message-center close
button, onboarding top bar, design-files toolbar upload trigger) so
none of this lands with a newly-red suite.
* perf(web): optimize onboarding cloud artwork
* fix(web): invalidate project file reads after mutations
* fix(web): disable readonly project starter mutations
* fix(web): preserve focus for standalone composer panels
* test(e2e): require explicit workspace scope
* fix(web): invalidate file reads on project events
* test(e2e): seed explicit settings workspace
* test(e2e): align manual edit with current inspector
* fix(web): refresh deployments when share opens
* test(e2e): follow composer design system entry
* fix(web): keep local project creation workspace-independent
* fix(web): preserve scoped auth recovery state
* test(e2e): follow current entry settings flows
* fix(web): preserve AMR retry across settings
* test(e2e): model explicit personal workspace recovery
* test(e2e): stabilize AMR logout settings recovery
* fix(projects): return workspace scope on create
---------
Co-authored-by: lefarcen <935902669@qq.com>
* test(e2e): lock design system team partition
* fix(workspace): stabilize scoped project lifecycle
* test(e2e): stabilize retained workspace transitions
* fix(workspace): retain exact scope through transient outages
* test(workspace): lock outage identity boundaries
---------
Co-authored-by: Ray Xi <2667192167@qq.com>
Co-authored-by: PerishFire <39043006+PerishCode@users.noreply.github.com>
Co-authored-by: Looper <looper@noreply.github.com>
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
Co-authored-by: bestthanapon <thanapon.suwannasuk@gmail.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: Amy <58060647+AmyShang-alt@users.noreply.github.com>
Co-authored-by: Marc Chan <mrc@powerformer.com>
Co-authored-by: _HDCoder <2728073932@qq.com>
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: Nicholas-Xiong <2482929840@qq.com>
Co-authored-by: mehmet turac <mehmetturac@gmail.com>
Co-authored-by: open-design-release-bot[bot] <295937643+open-design-release-bot[bot]@users.noreply.github.com>
Co-authored-by: open-design-bot <bot@open-design.ai>
Co-authored-by: CHENGLONG WANG <honam884844@gmail.com>
* fix(workspace): seed dashboard workspace input
* test(workspace): cover live dashboard workspace input
* fix(workspace): backfill legacy workspace names
* test(e2e): harden workspace collaboration coverage (#6312)
Harden Workspace collaboration authority, lifecycle cleanup, and browser coverage.\n\nRetry an expired authorized Team-project pull exactly once with a fresh receipt while preserving fail-closed scope, version, cleanup, and final freshness checks. Align the fake Vela receipt lifetime with the real two-second contract and use scalable E2E timeouts.
* fix(workspace): refresh dashboard input on switch
* fix(viewer): avoid inert attribute warning
* fix(chat): preserve user turn ordering
* fix(workspace): stop retracted Team plugins immediately (#6335)
Prevent stale Team-plugin listings, materializations, and delayed positive hub reads from reactivating a resource after retraction. Preserve independent Personal plugins with the same manifest ID and keep Team members able to share their own resources.
Add deterministic regression coverage for cache invalidation, binding lifecycle, and the superseded-positive activation race.
* fix(workspace): harden Vela billing proxy boundaries (#6348)
Reject invalid wallet balances without overwriting valid cache state, validate normalized proxy paths and Workspace scope, strip hop-by-hop headers, and tear down upstream requests on disconnect.
Keep team Workspace scope fail-closed when Connection nominates the protected header.
* fix(web): tolerate unnamed workspace directory rows (#6349)
Preserve a verified explicit Workspace context when legacy directory rows omit the display-only workspaceName field. Keep exact-row name backfill when a non-empty name is present, without inferring authority from active or default Workspace state.
* fix(workspace): enforce exact project-list scope
* fix(workspace): show team design systems in pickers
* fix(web): reserve image cards while thumbnails load
* fix(web): keep home creation types ready during refresh
* test(e2e): align Windows beta smoke with installer
* perf(web): start team catalog from exact directory identity
* fix(web): refresh revoked workspace context
* fix(web): order collaboration presence refreshes
* fix(amr): prefer selected Vela OpenCode companion
* fix(web): isolate collaboration presence sessions
* fix(web): parallelize scoped project route bootstrap
* perf(daemon): cache workspace project list authority
* fix(web): preserve project-open connection budget
* test(web): preserve memory toast exports in app mock
* fix(web): preserve exact project opening scope
* fix(daemon): coalesce scoped comment pulls
* fix(daemon): retry team comment relay delivery
* fix(daemon): atomically queue comment relay writes
* fix(collab): show team comments across local conversations
* fix(daemon): preserve catalog-only projects on unshare
* fix(collab): close comment realtime catch-up gaps
* docs: add workspace team rollout runbook
* fix(collab): retry failed Team project shares
* fix(workspace): isolate personal projects in recent lists
* fix(daemon): isolate personal design systems
* fix(workspace): authorize runs before plugin snapshots
* test(daemon): align workspace retry contracts
* fix: isolate plugins and skills by workspace member
* fix(workspace): scope design systems in run prompts
* fix(workspace): gate runs before plugin resolution
* fix(daemon): namespace team design system bindings
* fix: close plugin and skill scope leaks
* test(collab): cover project-scoped comment actions
* fix(workspace): scope project resource selection
* fix(workspace): preserve personal design system reads
* fix(workspace): authorize scoped run resources first
* fix(workspace): keep project design systems scoped
* fix(types): carry design system member scope
* fix(billing): scope personal balances by workspace
* fix(workspace): preserve unbound local design systems
* fix: resolve team design systems by canonical binding
* fix(workspace): invalidate nested project catalog cache
* fix: keep team design system runs canonical
* perf: batch durable comment relay delivery
* fix: cancel stale local comment relays
* fix(web): preserve scoped project display snapshots
* fix(web): synchronize project rename projections
* perf: reuse request-scoped workspace authority
* test: enforce personal project creator ownership
* fix(web): wait for project workspace authority
* fix(daemon): invalidate team project caches before signaling
* fix(web): order targeted project metadata refreshes
* fix(workspace): preserve team catalog on outages
* fix(web): isolate catalog display across accounts
* fix(web): scope extension marketplace catalogs
* fix(web): order broad team catalog refreshes
* fix(web): scope home plugins to workspace identity
* fix(web): isolate plugin views by workspace identity
* fix(web): polish sign-in and composer focus
* fix(web): preserve scoped project state after team moves
* fix(workspace): enforce scoped design and billing identity
* test(e2e): align workspace flows with scoped bootstrap
* fix(collab): drain newer comment revisions promptly
* test(e2e): keep owner actions after workspace lock
* fix(web): converge project renames across scoped lists
* fix(web): preserve pending projects across view changes
* fix(web): restore preview runtime after activation
* test(e2e): align workspace scope assertions
* test(web): cover hidden stream recovery
* fix(web): update deep-link project titles after rename
* fix(collab): retry project metadata propagation
* fix(web): preserve legacy design system logos
* fix(web): wait for activated preview bridge
* fix(design-systems): isolate team resource storage
* fix(plugins): reject team mirror mutations
* fix(web): partition extension catalogs by workspace
* fix(skills): isolate team mirrors by workspace
* fix(plugins): keep local share projects unscoped
* fix(web): retry preview state capture during activation
* test(e2e): scope project workspace fixtures explicitly
* feat(collab): coordinate team resource invalidations
* fix(collab): emit team resource changes after sync
* fix(web): invalidate project lists after updates
* fix(web): serialize project renames by scope
* fix(collab): preserve team project comment anchors
* fix(web): refresh shared resources after remote changes
* fix(collab): materialize team projects before rename
* fix(web): keep workspace catalogs current
* fix(web): close catalog and credential races
* test(workspace): harden team workflows and packaged deep links (#6358)
* test(workspace): harden team workflows and deep links
* fix(packaged): deliver hot invite deeplinks
Generated-By: looper 0.11.7 (runner=fixer, agent=codex)
* fix(workspace): catch up team resources safely
Generated-By: looper 0.11.7 (runner=fixer, agent=codex)
* fix(workspace): invalidate resource catch-up changes
Generated-By: looper 0.11.7 (runner=fixer, agent=codex)
* fix(workspace): catch up shared resource changes
Generated-By: looper 0.11.7 (runner=fixer, agent=codex)
* fix(packaged): preserve fallback invite deeplinks
Generated-By: looper 0.11.7 (runner=fixer, agent=codex)
* fix(workspace): hide retracted team skills
Generated-By: looper 0.11.7 (runner=fixer, agent=codex)
* test(e2e): align workspace CI fixtures with route bootstrap
* fix(collab): complete design system reconnect adoption
* feat(analytics): track Workspace interactions (#6397)
* feat(analytics): track Workspace interactions
* fix(analytics): dedupe community page views
---------
Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
* fix(daemon): preserve team comments across conversations
* fix(collab): keep presence rosters converged
* fix(comments): preserve routed comment anchors
* fix(workspace): reconcile team project renames
* fix(collab): preserve member directory last-good state
* perf(collab): start scoped Team presence before status
* fix(comments): keep relay anchors internal
* perf(collab): warm presence from daemon cache
* perf(collab): cancel obsolete cold status polls
* fix: fence presence sessions against late heartbeats
* perf(collab): share team member roster scheduling
* fix(collab): preserve roster store through strict replay
* perf(collab): retain workspace member directory stores
* fix(collab): resolve presence identities from team directory
* fix(collab): hide unresolved presence identities
* fix(collab): prefer directory presence names
* perf(web): preload workspace member directory
* test(collab): preserve no-leave lifecycle contract
* fix(web): preserve personal workspace presence after share
* fix(web): refresh covers after team materialization
* fix(web): refresh files after team materialization
* fix(web): gate shared file reads on project authority
* fix(web): preserve workspace folder action sizing
* ci(e2e): split project-workspace UI P0 shard into project-workspace + project-collab
project-workspace pins workers=1 by design (deliberate, not incidental —
see e2e/AGENTS.md's UI test stability rules on cross-file worker carry-over),
so its 7 files ran fully serially: ~26min wall time in recent merge-queue
runs, the longest pole in the ui_p0 matrix by a wide margin over its
10-13min siblings.
workspace-multi-client-collab.test.ts alone (spins up two isolated
client/daemon runtimes per case) accounted for ~10 of those minutes.
Carving it and workspace-keyboard-flows.test.ts into a new project-collab
shard — still workers=1, still one file at a time — lets the two halves
run as separate concurrent CI jobs instead of one long serial chain,
without touching the single-worker-per-shard isolation the pin exists for.
Updates the topology guards that pin the exact matrix shape so the split
doesn't drift silently: DAEMON_RUNTIME_DEFINITION_MATRIX_NAMES (scopes.ts),
the full/candidate matrix name guards (guard/scope.ts), and the fixed
expectations in scopes.test.ts / packaged-smoke-workflow.test.ts.
Validated: pnpm guard (UI P0 shadow contract + CI topology checks pass),
pnpm typecheck (all workspaces), and the two topology test files
(107/107 passing).
* fix(collab): revoke unshared team project mirrors
* fix(web): preserve unbound project authority across workspace changes
* fix(web): pair scoped raw queries with headers
* test(e2e): pin writable scope for project actions
* test(web): cover Personal authority recovery after reload
* test(web): await version preview readiness
* fix(workspace): scope brand design system resources
* fix(workspace): materialize owner team design systems
* fix(web): fence design system reads by generation
* fix(web): propagate brand read generations
* fix(web): preserve workspace project visibility
* fix(preview): retain scope for runtime relative assets
* fix(web): explain design system delete denial
* fix(daemon): bound team resource prewarm scopes
* fix(daemon): recheck prewarm lease after queue
* fix(web): stabilize transient presence roster gaps
* chore(pack): pin Vela CLI 0.0.28
* fix(workspace): move design system project with team share
* fix(workspace): gate linked unshare on live authority
* fix(workspace): ensure backing project before design share
* fix(web): scope srcdoc preview assets
* perf(web): mint preview scope only for srcdoc
* perf(web): reuse srcdoc preview scope
* chore(nix): refresh pnpm deps hashes for vela-cli 0.0.28
pnpm-lock.yaml moved @powerformer/vela-cli from 0.0.27-test.7 to 0.0.28, but nix/pnpm-deps.nix was not regenerated, so `nix flake check` failed with a fixed-output hash mismatch for both the daemon and web derivations.
Hashes taken from the Nix-reported expected values in CI run 30933223422 (nix is not available on this machine to run `pnpm nix:update-hash` locally); the merge of origin/main did not touch pnpm-lock.yaml, so those values still describe this tree.
* chore(ci): retrigger workspace-team validation
The previous ci run for this branch sat pending with zero dispatched jobs; cancelling it and pushing an empty commit re-enters the ci-6142 concurrency group from a clean slot.
* perf(web): skip preview mint for authored base
* fix(collab): preserve presence through lease gaps
---------
Co-authored-by: Ray Xi <2667192167@qq.com>
Co-authored-by: CHENGLONG WANG <honam884844@gmail.com>
Co-authored-by: lefarcen <ontf116@gmail.com>
Co-authored-by: PerishFire <39043006+PerishCode@users.noreply.github.com>
Co-authored-by: Looper <looper@noreply.github.com>
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
Co-authored-by: bestthanapon <thanapon.suwannasuk@gmail.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: Amy <58060647+AmyShang-alt@users.noreply.github.com>
Co-authored-by: Marc Chan <mrc@powerformer.com>
Co-authored-by: _HDCoder <2728073932@qq.com>
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: Nicholas-Xiong <2482929840@qq.com>
Co-authored-by: mehmet turac <mehmetturac@gmail.com>
Co-authored-by: open-design-release-bot[bot] <295937643+open-design-release-bot[bot]@users.noreply.github.com>
Co-authored-by: open-design-bot <bot@open-design.ai>
Co-authored-by: shangxinyu1 <shangxinyu@refly.ai>
Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
* feat(powered-preview): implement cross-origin isolation for WebGL and Worker artifacts
- Introduced a new powered preview feature that allows rendering HTML artifacts in a cross-origin-isolated iframe, enabling the use of Web Workers, WASM, and SharedArrayBuffer.
- Added API endpoint `/api/preview/isolation` to report the daemon's directly-reachable HTTP origin for rendering.
- Implemented logic in the file viewer to determine if an artifact requires powered preview based on its content.
- Updated the file serving routes to support powered preview, ensuring proper headers are set for cross-origin isolation.
- Enhanced the user interface to include new scenarios for WebGL and Worker visualizations in the home hero section, with corresponding translations and descriptions.
This feature addresses the limitations of the opaque-origin preview sandbox, allowing for more complex GPU and compute applications to function correctly.
* feat(preview): add WebGL templates and lazy HTML previews
* test(web): stabilize project back routing assertion
* fix(preview): address powered preview review blockers
* fix(preview): isolate powered preview origin
* fix(web): avoid duplicate style picker group keys
(cherry picked from commit efbe579df7)
* Fix Design Files and sidebar page layout regressions (#3629)
Fixes two web UI layout regressions:
- Prevent Design Files name cells from collapsing to near-zero width, which caused generated file and folder names to wrap vertically while leaving empty space to the right.
- Hide inactive entry views with display: none so Plugins, Integrations, Design Systems, and Automations no longer reserve hidden Home view height above their page content.
Validated with web typecheck, focused web component tests, pnpm guard, and local visual checks.
(cherry picked from commit 686a0b3702)
* fix(web): preserve failed retry attempts
(cherry picked from commit 0ef1400a6e)
* Auto-open produced HTML previews (#3636)
(cherry picked from commit 1fada1ec6d)
* Fix URL preview image export capture
(cherry picked from commit 4659926be8)
---------
Co-authored-by: yinjialu <2481044901@qq.com>
Co-authored-by: Ray Xi <109605599+Siri-Ray@users.noreply.github.com>
* feat(daemon): add interactive terminal support with node-pty
- Introduced a new terminal service to manage interactive terminal sessions.
- Added routes for creating, streaming, and managing terminal sessions in the daemon.
- Integrated terminal functionality into the CLI, allowing users to open interactive shells.
- Updated project routes to support conversation seeding for side chats.
- Enhanced the web application to include terminal tab functionality and UI components.
This feature enables users to interact with a terminal directly within the application, enhancing the overall user experience and providing a more integrated development environment.
* feat(web): implement embedded browser module in Design Files workspace
- Added a `+` icon to the Design Files tab for opening a new Browser module.
- The Browser module supports navigation features including back, forward, refresh, and address input.
- Integrated a curated list of design reference URLs for user convenience.
- Implemented browser data clearing functionality via IPC.
- Enhanced desktop runtime to support embedded browser with appropriate security measures.
- Added tests for browser functionality and URL handling.
This commit establishes a new workspace for browsing and referencing design resources directly within the application, improving user experience and accessibility to design tools.
* refactor(web): enhance add tab menu functionality in FileWorkspace
- Updated the add tab button to toggle the visibility of the menu with improved accessibility attributes.
- Refactored the add menu to be portaled to the document body for better positioning and visibility.
- Adjusted CSS styles for the add menu to use fixed positioning and increased z-index for proper layering.
- Minor CSS adjustments in entry layout for consistent padding.
These changes improve the user experience when adding new modules in the FileWorkspace, ensuring the add menu is more accessible and visually consistent.
* feat(daemon): introduce session mode for conversations
- Added a new `session_mode` column to the `conversations` table with a default value of 'design'.
- Implemented logic to handle `session_mode` in conversation creation, updates, and retrieval.
- Enhanced the API to support `session_mode` in conversation requests, allowing for 'chat' or 'design' modes.
- Updated the web application to include a session mode toggle, enabling users to switch between chat and design modes seamlessly.
- Adjusted system prompts to reflect the current session mode, providing context-aware responses.
This feature enhances the user experience by allowing for more flexible conversation management, catering to different interaction styles.
* feat(web): enhance navigation and settings functionality in DesignBrowserPanel and EntryShell
- Introduced a navigation stack in DesignBrowserPanel to manage back and forward navigation states.
- Updated the browser navigation logic to handle URL history and improve user experience.
- Added a settings menu in EntryShell for quick access to language and appearance options.
- Implemented CSS styles for the new settings menu, ensuring a consistent and user-friendly interface.
- Enhanced tests for navigation functionality and settings menu interactions.
These changes improve the overall usability of the application by streamlining navigation and providing easy access to settings.
* feat(daemon): enhance conversation session mode handling
- Added a new `mode` flag to CLI commands for project and conversation creation, allowing users to specify 'design' or 'chat' modes.
- Implemented `normalizeChatSessionModeFlag` function to validate and normalize session mode inputs.
- Updated project routes to handle session mode during conversation creation and updates.
- Enhanced web components to support session mode changes, including new props and handlers for managing session modes in conversations.
- Adjusted UI elements to reflect the current session mode, improving user experience and interaction flexibility.
This update provides a more robust framework for managing conversation modes, catering to diverse user needs and enhancing overall functionality.
* feat(web): enhance HandoffButton and DesignBrowserPanel with improved functionality and styling
- Updated HandoffButton to support framework-specific CLI prompts and improved local project path handling.
- Enhanced DesignBrowserPanel to manage browser history with favicon support and improved address display.
- Introduced new utility functions for formatting addresses and extracting hostnames.
- Refactored CSS styles for better layout and responsiveness across components.
- Added tests for new functionalities in HandoffButton and DesignBrowserPanel, ensuring robust behavior.
These changes improve user experience by streamlining the handoff process and enhancing the design browsing capabilities within the application.
* feat(web): enhance HandoffButton and ProjectView with improved instructions handling and UI updates
- Updated HandoffButton to include a tabbed interface for switching between editor and CLI options, enhancing user experience.
- Added support for opening the AMR website directly from the HandoffButton.
- Refactored ProjectView to implement a modal for custom instructions, allowing users to edit and review instructions more intuitively.
- Improved CSS styles for project instructions modal and handoff menu, ensuring better layout and responsiveness.
- Added keyboard accessibility for closing the instructions modal with the Escape key.
These changes streamline the handoff process and improve the usability of custom instructions within the application.
* feat(daemon): add social share functionality and project folder management
- Introduced new `share` command in CLI for building localized social-share targets for Open Design projects.
- Implemented `printShareUsage` and `runShare` functions to handle share requests and display usage instructions.
- Added API routes for social sharing, allowing users to create shareable links for projects.
- Enhanced project routes with new endpoints for listing and creating project folders, improving project organization.
- Updated relevant files and tests to support new functionalities, ensuring robust behavior.
These changes enhance user experience by facilitating social sharing and better project management within the application.
* feat(web): enhance DesignBrowserPanel and DesignFilesPanel with improved address formatting and drag-and-drop functionality
- Refactored `formatAddressDisplay` to utilize a new `formatAddressDisplayParts` function, separating URL and title handling for better clarity.
- Updated `DesignFilesPanel` to improve drag-and-drop interactions, including enhanced directory navigation and file management features.
- Adjusted CSS styles for better visual consistency and responsiveness across components.
- Added tests for new functionalities in `DesignBrowserPanel` and `DesignFilesPanel`, ensuring robust behavior.
These changes improve user experience by streamlining address formatting and enhancing file management capabilities within the application.
* feat(web): enhance DesignBrowserPanel and DesignFilesPanel with new reference categories and improved folder creation
- Added new reference categories in DesignBrowserPanel, including 'Inspiration', 'Real Interfaces', 'Color', 'Typography', and more, each with curated design resources.
- Improved folder creation logic in DesignFilesPanel to suggest names based on existing folders, enhancing user experience.
- Updated CSS styles for better layout and responsiveness across components, particularly in control rows and search functionalities.
- Added tests for new reference categories and folder creation features, ensuring robust functionality.
These changes enrich the design resource catalog and streamline folder management, improving overall usability within the application.
* feat(web): implement reference icon functionality and enhance social share features
- Replaced favicon URL generation with a new `referenceIconUrl` function to provide reliable icons for curated design sites, improving visual consistency in the DesignBrowserPanel.
- Updated the FileViewer component to enhance social share functionality, including clearer messaging for protected deployments and improved UI for sharing links.
- Added CSS styles to support new visual states for social share components, ensuring better user experience.
- Expanded tests for the new `referenceIconUrl` function and social share interactions, ensuring robust functionality.
These changes enhance the design resource presentation and improve the sharing experience within the application.
* feat(web): enhance DesignFilesPanel and FileViewer with improved folder management and social share functionality
- Added optimistic folder path management in DesignFilesPanel to improve user experience during folder creation.
- Updated social share logic in FileViewer to handle protected deployments more effectively, ensuring clearer messaging and UI updates.
- Refactored CSS styles for better layout and responsiveness in both components, enhancing overall usability.
- Expanded tests for new folder management features and social share interactions, ensuring robust functionality.
These changes streamline folder management and enhance the social sharing experience within the application.
* feat(daemon): enhance project tab management with new state handling
- Updated database schema to include a new `state_json` column in the `tabs_state` table for improved project tab state management.
- Implemented functions to normalize and parse project tab states, including handling browser workspace tabs.
- Modified `listTabs` and `setTabs` functions to utilize the new state management features, allowing for better tracking of active tabs and saved states.
- Refactored related types in the contracts and web applications to support the new tab state structure.
These changes improve the functionality and user experience of managing project tabs within the application.
* feat(web): enhance project tab management and browser tab functionality
- Updated project routes and server logic to include `browserTabs` in the request body, improving tab state management.
- Implemented validation for `browserTabs` to ensure it is an array, enhancing error handling.
- Refactored `setTabs` function to accommodate the new structure for managing tabs, including browser-specific tabs.
- Added tests for browser tab state persistence and management in the new `project-tabs-state.test.ts` file, ensuring robust functionality.
These changes improve the user experience by providing better management and persistence of project and browser tabs within the application.
* feat(web): enhance ProjectView with improved chat send management and UI updates
- Added support for queue-only chat sends, allowing for better handling of messages during busy conversations.
- Refactored chat send functions to improve state management and persistence of queued messages.
- Updated CSS styles in the DesignFilesPanel for better layout and responsiveness, including search control enhancements.
- Expanded tests for new chat send functionalities and search interactions, ensuring robust behavior.
These changes improve the user experience by streamlining chat interactions and enhancing the overall design file management interface.
* feat(web): improve project tab state management and caching
- Enhanced the `loadTabs` function to utilize cached tab states, improving performance and user experience during data fetching.
- Implemented `normalizeTabsState` and caching functions to manage tab states effectively, including validation and error handling.
- Updated `writeCachedTabs` to ensure the latest state is stored in local storage, facilitating better persistence of tab information.
- Modified `listTabs` in the daemon to include `updatedAt` in the state retrieval, allowing for more accurate tracking of tab updates.
These changes streamline tab management and enhance the overall responsiveness of the application.
* feat(web): integrate Lexical for enhanced text composition and mention handling
- Added Lexical as a dependency to improve text composition capabilities within the chat interface.
- Implemented mention functionality with the creation of `MentionNode` and related serialization/deserialization logic for inline mentions.
- Enhanced `ChatComposer` and `ChatPane` components to support queue-only message sending and improved state management for queued messages.
- Updated `DesignBrowserPanel` and `PreviewDrawOverlay` to incorporate new features for capturing and annotating browser snapshots.
- Refactored various components to streamline interactions and improve user experience during chat and design tasks.
These changes significantly enhance the text editing experience and provide better management of chat interactions, improving overall usability in the application.
* feat(workspace): integrate terminal viewer and enhance chat functionality
- Added a new terminal viewer component with customizable themes and improved styling for better user experience.
- Integrated terminal functionality into the workspace, allowing users to interact with a terminal directly within the application.
- Updated chat components to support active conversation states, enabling seamless message handling and interaction.
- Refactored chat-related props and state management to enhance performance and maintainability.
- Removed deprecated file tree explorer components to streamline the workspace interface.
This update enhances the overall functionality of the workspace, providing users with a more integrated and responsive environment for both terminal and chat interactions.
* feat(workspace): enhance chat and session mode functionality
- Introduced a new `+` launcher in the workspace for easy access to project files and new tabs, including Side Chat and Terminal.
- Added Side Chat functionality that allows users to create context-aware conversations based on existing chats.
- Implemented a new terminal tab type for interactive terminal sessions using `node-pty`, enabling users to run shell commands directly within the workspace.
- Enhanced chat functionality with a new session mode toggle, allowing users to switch between 'design' and 'chat' modes seamlessly.
- Added a feature to copy chat responses as markdown to the clipboard, improving usability and sharing capabilities.
- Updated various components and styles to support these new features, ensuring a cohesive user experience.
This update significantly improves the workspace's interactivity and usability, providing users with more tools for collaboration and development.
* feat(workspace): enhance session mode toggle and chat functionality
- Improved the SessionModeToggle component to include localized guidance cards that provide context-aware descriptions for each mode.
- Updated the UI to display a popover with mode descriptions when hovering over options, enhancing user understanding of the available modes.
- Added a new copy button in the AssistantMessage footer to allow users to copy responses as raw Markdown, improving usability for external documentation.
- Enhanced localization support by updating i18n keys and translations for various languages, ensuring consistent user experience across different locales.
- Refactored styles for the session mode toggle and associated components to improve layout and responsiveness.
This update significantly enhances the user experience by providing clearer guidance and improved functionality in chat interactions.
* feat(styles): enhance session mode toggle styling for improved visibility
- Added new CSS rules to ensure the session mode toggle popover and hover cards are displayed correctly with increased z-index and visibility.
- Updated styles for various chat components to maintain consistent positioning and overflow behavior when session mode elements are present.
- Improved overall layout responsiveness for chat interactions, enhancing user experience during mode transitions.
This update refines the visual presentation of session mode toggles, ensuring they are more accessible and user-friendly.
* feat(web): finish Lexical composer input with atomic mention pills
* fix(web): make browser reference board scroll with a pinned toolbar
The browser tab's default Reference Board (.db-start) is wrapped by
PreviewDrawOverlay's position:absolute container, which is not a flex
parent — so .db-start's flex:1 1 auto never bounded its height and the
board grew to its content height instead of scrolling (and the sticky
.db-reference-toolbar could not pin). Fill the overlay with height:100%
like the .db-webview/.db-fallback siblings already do, restoring scroll
and the sticky toolbar.
* test(daemon): expect updatedAt in persisted tab state round-trip
listTabs() returns the tabs_state row's updatedAt timestamp on the
saved-state path, but these two toEqual assertions predated that field
and failed strictly. Match the real shape with expect.any(Number).
* feat(chat): enhance ChatComposer with session mode management and UI improvements
- Introduced a new `sessionMode` prop to the ChatComposer, allowing users to switch between different session modes (e.g., 'design').
- Added a SessionModeToggle component for improved user interaction and visibility of session options.
- Updated the ToolsTab type to reflect the removal of the pet option, streamlining the tools available in the composer.
- Refactored styles to enhance the visibility and positioning of session mode elements, ensuring a better user experience during mode transitions.
- Improved the handling of draft state and user interactions within the composer, enhancing overall functionality.
These changes significantly improve the ChatComposer's usability and flexibility, providing users with clearer options and a more responsive interface.
* feat(chat): implement caret floating layer for mention and slash popovers
- Introduced a new `CaretFloatingLayer` component to manage the positioning of mention and slash command popovers relative to the caret.
- Enhanced `ChatComposer` to utilize the caret rectangle for accurate popover placement, improving user experience during text input.
- Updated `LexicalComposerInput` to pass caret position data to the trigger handling logic, allowing for dynamic popover adjustments.
- Refactored styles for popovers to ensure consistent appearance and behavior, including improved animations and responsiveness.
- Added accessibility features to mention and slash popovers, enhancing usability for keyboard navigation.
These changes significantly improve the interaction model for mentions and commands within the chat interface, providing a more intuitive and responsive user experience.
* feat(chat): update mention tab order and improve search functionality
- Reordered the mention tab sections in the ChatComposer to prioritize 'Design files' over other categories, enhancing user experience during mentions.
- Updated the search prompt to reflect the new tab order, ensuring clarity in search functionality.
- Enhanced the mention selection logic to accommodate the new tab structure, allowing for a more intuitive navigation experience.
- Added tests to verify the correct display and functionality of the updated mention tabs and search behavior.
These changes significantly improve the usability of the mention feature within the chat interface, making it easier for users to find and select relevant items.
* feat(chat): enhance context management in ChatComposer and HomeHero
- Added functionality to manage MCP servers and connectors within the ChatComposer, allowing users to remove these contexts seamlessly.
- Updated the HomeHero component to support the selection and removal of MCP servers and connectors, improving context handling in user interactions.
- Enhanced the search prompt to include files, ensuring users can search across all relevant categories.
- Refactored related components and styles for better integration and user experience.
- Added tests to verify the correct functionality of the new context management features.
These changes significantly improve the usability of context features in the chat interface, making it easier for users to manage their interactions effectively.
* feat(chat): enhance message handling with session mode and plugin snapshot support
- Added new columns to the database schema for `session_mode` and `applied_plugin_snapshot_json` to support enhanced message context.
- Updated the `upsertMessage` and `listMessages` functions to handle the new fields, ensuring messages can store and retrieve session mode and plugin snapshot data.
- Enhanced the `ChatComposer` to manage and send the applied plugin snapshot as part of the message context, improving user interaction with plugins.
- Introduced a new `MessageSessionModeChip` component to visually represent the session mode in the chat interface.
- Updated styles for better presentation of session mode and plugin context within messages, enhancing user experience.
These changes significantly improve the context management capabilities in the chat interface, allowing for richer interactions and better tracking of session-specific data.
* feat(chat): add annotation handling and UI improvements in ChatComposer and PreviewDrawOverlay
- Implemented functionality to stage draw annotations into the composer input without sending, enhancing user interaction.
- Added a new button in the PreviewDrawOverlay to allow users to append notes to the input, improving workflow flexibility.
- Updated the ChatComposer tests to verify the correct staging of annotations and their integration into the input.
- Enhanced internationalization support by adding new translation keys for annotation actions across multiple languages.
These changes significantly improve the user experience by providing more intuitive annotation handling and better integration within the chat interface.
* feat(capture): implement page capture functionality and enhance folder management dialogs
- Added new capture functionality to allow users to take snapshots of the current page, improving user interaction with visual content.
- Introduced in-app dialogs for folder creation and moving files, replacing the unsupported window.prompt in the Electron desktop host, enhancing usability across platforms.
- Updated the DesignFilesPanel to support these new dialogs, ensuring a seamless experience for managing project files.
- Enhanced internationalization support by adding new translation keys for folder management actions across multiple languages.
These changes significantly improve the user experience by providing intuitive capture options and streamlined file management within the application.
* feat(projects): implement folder deletion and enhance project file management
- Added a new API endpoint to delete project folders, improving the file management capabilities within the application.
- Introduced utility functions for ensuring project subdirectories and safely deleting folders, enhancing the robustness of folder operations.
- Updated the DesignFilesPanel and FileWorkspace components to support folder deletion actions, providing users with a more intuitive interface for managing project files.
- Enhanced internationalization support by adding new translation keys for folder management actions.
These changes significantly improve the user experience by streamlining folder management and providing clearer options for users to organize their projects effectively.
* feat(composer): enhance BoardComposerPopover with image attachment functionality
- Added support for attaching images to comments, allowing users to upload and preview images directly within the composer.
- Implemented new handlers for image input changes and clipboard pasting, improving the user experience for image uploads.
- Updated the component's props to include image-related callbacks and state management for attached images.
- Enhanced styles for image thumbnails and removal buttons, ensuring a cohesive design with the existing comment popover interface.
These changes significantly improve the functionality of the BoardComposerPopover, providing users with a more interactive and visually rich commenting experience.
* feat(file-viewer): enhance comment attachment functionality with image support
- Updated the `onSendBoardCommentAttachments` prop to accept an additional `images` parameter, allowing for image attachments alongside comments.
- Introduced state management for handling attached images, including functions to add and remove images, and to generate previews.
- Implemented a modal for previewing attached images, improving user interaction when managing comment attachments.
- Updated the `FileWorkspace` component to reflect changes in the props, ensuring consistency across components.
These enhancements significantly improve the commenting experience by enabling users to attach and preview images directly within the file viewer.
* feat(home-hero, project-view, styles): enhance functionality and user experience
- Updated the HomeHero component to prevent unnecessary state changes during programmatic updates, improving user interaction with prompts.
- Enhanced the ProjectView component to support image attachments alongside comments, allowing for a more versatile commenting experience.
- Implemented a new image upload process that queues tasks efficiently, ensuring smooth handling of comment attachments.
- Added CSS to reserve scrollbar space in design files, preventing layout shifts when scrollbars appear, thus enhancing visual stability.
These changes collectively improve the user experience by streamlining interactions and ensuring consistent UI behavior across components.
* feat(preview-comments, chat): enhance comment functionality with attachment support
- Added support for `attachments_json` in the `preview_comments` table, allowing users to attach files to comments.
- Updated relevant functions to handle attachments, including `upsertPreviewComment` and `listPreviewComments`, ensuring attachments are properly managed and displayed.
- Enhanced the `CommentSidePanel` to render attached files, providing users with a visual representation of their attachments.
- Improved the `BoardComposerPopover` and `ChatPane` components to support image attachments, including drag-and-drop functionality for reordering queued sends.
These changes significantly enhance the commenting experience by enabling users to attach and manage files directly within the chat interface, improving overall usability and interaction.
* refactor(comment-attachments): rename attachment normalization functions for clarity
- Updated the `normalizeCommentAttachments` function to `normalizePreviewCommentAttachments` for better context in handling preview comment attachments.
- Adjusted the `upsertPreviewComment` and `normalizePreviewComment` functions to utilize the renamed attachment normalization function.
- Added tests to ensure that image attachments are correctly saved and retrieved, addressing a regression issue with attachment persistence.
These changes enhance code clarity and maintainability while ensuring the functionality for handling comment attachments remains robust.
* feat(comment-attachments): enhance comment submission with image and note validation
- Updated the `upsertPreviewComment` function to require either a text note or at least one image attachment for comment submission, improving validation logic.
- Modified the `BoardComposerPopover` to allow saving comments with only images, enhancing user experience by simplifying the commenting process.
- Adjusted the `FileViewer` to support saving comments with image-only notes, ensuring consistency across components.
- Improved styles in the chat and home hero components for better visual representation of attachments and comments.
These changes collectively enhance the commenting functionality, providing users with more flexibility in how they submit comments while ensuring robust validation.
* feat(project-view): implement auto-start for queued chat sends
- Replaced the `startingQueuedChatSendIdRef` with a state variable `queuedAutoStartBlocked` to manage the auto-start behavior of queued chat sends.
- Updated the `useEffect` to ensure that queued sends are processed one at a time after the active conversation completes, enhancing the chat experience.
- Added a test to verify that queued sends auto-start correctly after the active run finishes, ensuring reliable functionality.
These changes improve the handling of queued chat messages, providing a smoother user experience during conversations.
* feat(project-view): refine auto-start logic for queued chat sends
- Replaced the state variable `queuedAutoStartBlocked` with a reference `startingQueuedChatSendIdRef` to manage the auto-start behavior of queued chat sends more effectively.
- Updated the `useEffect` to ensure that queued sends are processed sequentially, improving the handling of chat messages during active conversations.
- Introduced a new state variable `queuedAutoStartTick` to track the auto-start process, enhancing the responsiveness of the chat interface.
These changes improve the reliability and user experience of the chat functionality by ensuring queued messages are handled smoothly and efficiently.
* feat(comment-attachments): improve attachment handling in preview comments
- Updated the `upsertPreviewComment` function to merge existing and incoming attachments, ensuring that image attachments are preserved when updating comments without new files.
- Introduced a new helper function, `mergePreviewCommentAttachments`, to handle the merging of attachments without duplicates, enhancing the robustness of attachment management.
- Added tests to verify the correct merging of attachments and the preservation of existing attachments during comment updates, improving overall functionality and user experience.
These changes enhance the commenting system by providing better management of image attachments, ensuring users can update comments seamlessly while retaining their attached images.
* feat(BoardComposerPopover): enhance popover positioning and measurement
- Updated the `popoverAnchorStyle` function to incorporate viewport scroll positions, ensuring the popover remains within visible bounds.
- Introduced a new `PopoverSize` type to manage measured dimensions, improving the accuracy of popover placement.
- Implemented a `useLayoutEffect` to dynamically measure the popover size and adjust its position accordingly, enhancing user experience during interactions.
- Added tests to verify that the popover correctly adjusts its position based on target and viewport dimensions, ensuring it remains fully visible.
These changes improve the usability of the BoardComposerPopover by ensuring it is properly positioned within the viewport, enhancing the overall commenting experience.
* feat(comment-attachments): enhance image attachment handling in comments
- Updated the `normalizeCommentAttachments` function to include image attachments in the comment payload, allowing comments to be submitted with only images.
- Introduced a fallback message for comments that consist solely of image attachments, improving user guidance.
- Enhanced the `renderCommentAttachmentHint` function to display image attachment details, ensuring users are informed about attached images.
- Added tests to verify that image attachments are preserved in comment submissions and correctly rendered in hints, improving overall functionality and user experience.
These changes enhance the commenting system by providing better support for image-only comments, ensuring users can effectively utilize image attachments in their interactions.
* feat(comment-attachments): refine comment context handling and enhance drag-and-drop functionality
- Updated the `normalizeCommentAttachments` function to conditionally omit comment text when the context is set to 'query', improving clarity in comment submissions.
- Enhanced the `renderCommentAttachmentHint` function to only display comments when not in 'query' context, ensuring a cleaner output.
- Implemented drag-and-drop functionality in the `CommentSidePanel` for reordering comments, improving user interaction and organization of comments.
- Added tests to verify the correct handling of comment context and the functionality of the drag-and-drop feature, ensuring robust performance.
These changes enhance the commenting system by providing clearer context management and improved usability through drag-and-drop capabilities.
* feat(FileViewer, ProjectView): enhance comment attachment handling and status updates
- Updated the `FileViewer` component to manage the state of sent comment IDs, ensuring that active previews are correctly updated after sending comments.
- Refined the `ProjectView` component to filter out comment attachments from the board-batch source and update their status to 'applying' during processing, improving user feedback on attachment handling.
- Introduced logic to handle the removal of sent comment IDs from the preview, enhancing the overall user experience when managing comments and attachments.
These changes improve the functionality and responsiveness of the commenting system, providing clearer feedback and better management of comment attachments.
* feat(conversation-forking): introduce message-based conversation forking
- Added the ability to fork conversations from a specific message, enhancing the chat experience by allowing users to create new conversations that inherit context up to a chosen point.
- Updated the CLI commands and help documentation to reflect the new `--fork-after` option, which specifies the message ID to stop copying from.
- Enhanced the backend to handle the new forking logic, ensuring that only messages up to the specified ID are included in the new conversation.
- Implemented tests to verify the forking functionality, ensuring robust performance and correct behavior when forking conversations.
These changes improve the flexibility of conversation management, allowing users to create tailored discussions based on previous interactions.
* feat(terminal-service): enhance session output management and memory handling
- Introduced new parameters for managing session output, including `maxBufferBytes`, `exitTailBytes`, `flushIntervalMs`, and `flushThresholdBytes`, to optimize memory usage and performance.
- Implemented a `trimBuffer` function to efficiently evict old events based on byte and count limits, improving memory management during active sessions.
- Added logic to coalesce buffered PTY output into single `data` events, reducing the frequency of event emissions and enhancing performance during high-throughput scenarios.
- Updated session event structure to include `byteLength`, allowing for better tracking of output size and memory usage.
These changes improve the efficiency and responsiveness of terminal sessions, ensuring better resource management and user experience.
* feat(workspace-context): enhance project attachment handling and workspace context management
- Introduced `formatProjectAttachmentHint` function to render project attachments in a user-visible order, improving clarity for users referencing attachments.
- Added `normalizeWorkspaceContextItems` function to standardize workspace context items, ensuring consistent handling of various context types.
- Updated `mergeRunContextSelections` to include workspace items, enhancing the context management during chat interactions.
- Enhanced `renderRunContextPrompt` to display active workspace context, providing users with better visibility of their current workspace state.
- Implemented tests for new functions to ensure robust functionality and correct behavior in handling project attachments and workspace contexts.
These changes improve the user experience by providing clearer context and better management of project attachments within the application.
* feat(database, chat): enhance message and conversation data structure
- Added `run_context_json` to the `messages` table schema to store contextual information for each message.
- Updated migration logic to include the new `run_context_json` field and ensure backward compatibility.
- Enhanced conversation retrieval to include `messageCount`, providing better insights into the number of messages per conversation.
- Improved attachment handling in the `ChatComposer` component by introducing ordering logic for attachments, ensuring a consistent user experience.
- Refactored `SessionModeToggle` to simplify state management and improve tooltip visibility.
These changes enhance data management and user interaction within the chat application, providing clearer context and improved functionality.
* feat(attachment-handling): improve attachment sorting and context management
- Introduced `sortAttachmentsByUserOrder` function to ensure attachments are displayed in a user-defined order, enhancing clarity and usability.
- Updated `historyWithApiAttachmentContext` to utilize the new sorting function, improving the context provided with message histories.
- Refactored `buildAnthropicMessageContent` to apply sorting for image attachments, ensuring consistent handling across different message types.
These changes enhance the user experience by providing better organization and visibility of attachments within the chat application.
* feat(chat): enhance conversation listing and loading states
- Improved the SQL query for listing conversations to include message counts, providing better insights into conversation activity.
- Added loading states to the ChatPane component, enhancing user experience during data fetching.
- Implemented a search feature in the conversation history, allowing users to filter conversations by title for easier navigation.
- Updated styles for loading indicators and conversation list to improve visual feedback during loading states.
These changes enhance the usability and responsiveness of the chat interface, providing users with clearer context and improved interaction capabilities.
* feat(chat): optimize suggestion filtering and enhance design system integration
- Refactored suggestion filtering in the ChatComposer component to utilize `useMemo`, improving performance by memoizing results based on dependencies.
- Added new props in ChatPane for handling plugin and design system details, enhancing the integration of these features within the chat interface.
- Updated the FileWorkspace component to manage tab states for design system and browser tabs, improving user navigation and context management.
- Introduced modals for displaying plugin and design system details in the ProjectView, enhancing user experience by providing contextual information.
These changes improve the efficiency of suggestion handling and enhance the overall user experience in managing plugins and design systems within the chat application.
* fix(chat): adjust styling and layout for improved chat interface
- Reduced the conversation row height in ChatPane for better alignment with design specifications.
- Updated CSS styles across various components to enhance layout consistency, including adjustments to margins, padding, and flex properties.
- Improved visual feedback and responsiveness in chat elements, ensuring a more cohesive user experience.
These changes refine the chat interface, making it more visually appealing and user-friendly.
* feat(comments): implement structural equality checks for comment snapshots
- Added `commentSnapshotOverlayEqual` and `commentSnapshotEqual` functions to compare comment snapshots based on their structural properties, improving performance by avoiding unnecessary state updates in the `FileViewer` component.
- Updated `HtmlViewer` to utilize these equality checks, optimizing the handling of live comment targets during pointer movements and hover events.
- Enhanced the overall responsiveness of the comment system by preventing redundant re-renders when comment snapshots remain unchanged.
These changes enhance the efficiency of comment handling and improve user experience in the commenting interface.
* feat(chat): enhance comment attachment sorting and order management
- Introduced `sortChatCommentAttachmentsByOrder` function to ensure comment attachments are displayed in a user-defined order, improving clarity and usability.
- Updated the `currentCommentAttachments` function to utilize the new sorting logic, enhancing the organization of attachments.
- Adjusted the order assignment for visual attachments to ensure consistent handling based on existing attachment orders.
These changes improve the user experience by providing better organization and visibility of comment attachments within the chat application.
* feat(chat): enhance queued send strip with overflow handling and styling improvements
- Added overflow handling to the queued send strip, allowing for better visibility of additional queued items when the list exceeds the visible limit.
- Updated CSS styles for the chat components, including adjustments to layout, padding, and font sizes to improve overall aesthetics and usability.
- Refactored the structure of the queued send row to utilize a grid layout, enhancing alignment and responsiveness of the elements within the chat interface.
These changes improve the user experience by providing clearer visibility of queued messages and a more polished interface.
* feat(database): add index for created_at and enhance comment retrieval
- Introduced a new index on `preview_comments` for `created_at` to optimize query performance when retrieving comments.
- Updated the `listPreviewComments` function to order results by `created_at` and `rowid`, improving the organization of displayed comments.
- Enhanced the test suite to verify the injection of the new URL preview selection bridge and its functionality in various scenarios.
These changes improve the efficiency of comment retrieval and enhance the user experience in the commenting interface.
* feat(tooltip): implement tooltip system for enhanced user guidance
- Introduced a new `TooltipLayer` component to manage tooltip display across the application, improving user interaction by providing contextual information on hover and focus.
- Updated various components to utilize the tooltip system, including buttons and icons, ensuring consistent tooltip behavior and styling.
- Enhanced CSS styles for tooltips, improving visibility and responsiveness, while maintaining a cohesive design across the application.
These changes enhance the user experience by providing clearer guidance and improving the overall usability of interactive elements.
* feat(tooltip): enhance tooltip integration across components
- Updated various components to include tooltip functionality, improving user guidance with contextual information on hover and focus.
- Added `data-tooltip` and `data-tooltip-placement` attributes to buttons and interactive elements for consistent tooltip behavior.
- Enhanced CSS styles to ensure tooltips are displayed correctly and responsively, maintaining a cohesive design across the application.
These changes improve the overall user experience by providing clearer guidance and enhancing the usability of interactive elements.
* feat(project-view): refactor comment handling and enhance tooltip positioning
- Introduced `mergeSavedPreviewComment` function to streamline the management of preview comments, improving clarity and maintainability.
- Updated `ProjectView` to utilize the new comment merging function, enhancing the efficiency of comment updates.
- Refactored `TooltipLayer` to use x and y coordinates for positioning, improving tooltip display accuracy and responsiveness.
- Enhanced CSS styles for tooltips, ensuring better visibility and layout consistency across the application.
These changes improve the user experience by providing more efficient comment handling and refined tooltip interactions.
* feat(design-files): enhance workspace hint and file handling in project context
- Introduced `formatDesignFilesWorkspaceHint` function to provide a detailed overview of the current Design Files workspace, including folder and file listings.
- Added limits for the number of folders and files displayed to improve clarity and prevent overwhelming users with excessive information.
- Updated the `startServer` function to integrate the new workspace hint, ensuring that the context of existing project files and folders is communicated effectively.
- Enhanced tests for the new workspace hint functionality to ensure accurate representation of project context.
These changes improve user experience by providing clearer insights into the Design Files workspace and facilitating better project management.
* feat(composer): atomic @mention keyboard navigation and deletion
* feat(database): add attachments_json field to comments and enhance migration logic
- Introduced `attachments_json` field in the database schema for comments to support attachment storage.
- Updated migration functions to include the new field, ensuring existing data is properly migrated.
- Refactored related SQL queries to accommodate the new field, improving data handling for comments.
These changes enhance the comment functionality by allowing attachments to be stored and retrieved effectively.
* chore(nix): refresh pnpm deps hash
* feat(chat-composer): implement design toolbox for enhanced design actions
- Added a new design toolbox feature to the ChatComposer, allowing users to access various design actions such as 'auto-match', 'motion', and 'visual-polish'.
- Introduced a state management system for the design toolbox, including hooks for opening and closing the toolbox.
- Enhanced the user interface with new components and styles for the design toolbox, improving accessibility and usability.
- Implemented functionality to apply design actions directly from the toolbox, streamlining the design workflow.
- Added tests to ensure the correct behavior of the design toolbox and its interactions within the ChatComposer.
These changes significantly enhance the design capabilities within the ChatComposer, providing users with a more efficient and intuitive design experience.
* feat(chat-composer): enhance design toolbox resource management
- Expanded the design toolbox functionality in ChatComposer to include a comprehensive resource index, allowing for better organization and retrieval of skills, plugins, MCP servers, templates, connectors, and project files.
- Introduced new types and interfaces to support the expanded resource management, improving type safety and clarity in the codebase.
- Updated the design toolbox action descriptions and search functionality to reflect the new resource capabilities, enhancing user experience.
- Added tests to validate the new resource indexing and search features, ensuring robust functionality.
These enhancements significantly improve the design workflow by providing users with a more organized and efficient way to access various design resources.
* feat(workspace): enhance workspace context management and UI integration
- Introduced a new function `renderWorkspaceContextToolHints` to provide contextual hints based on the type of workspace items (browser, terminal, files, live artifacts).
- Updated `ChatComposer`, `ChatPane`, and `FileWorkspace` components to support and display workspace context items, improving user interaction and accessibility.
- Enhanced the `QuickSwitcher` and `TabLauncherMenu` components to include workspace context items in search results, allowing users to navigate between tabs and files more efficiently.
- Added new translations and updated existing ones to reflect the inclusion of workspace tabs in the user interface.
These enhancements significantly improve the usability and functionality of the workspace, providing users with better context and navigation options.
* test(e2e): assert Lexical composer content with toHaveText, not toHaveValue
The chat composer and home hero input are now Lexical contenteditable
editors, not native form controls, so Playwright's toHaveValue (form-only)
fails with "Not an input element". Switch all chat-composer-input and
home-hero-input content assertions to toHaveText, and assert multi-line
soft-break cases with separate toContainText checks since the editor's
textContent collapses the newline (the newline reaching the sent payload
is already covered by downstream message/payload assertions).
* chore(nix): refresh pnpm deps hash
* chore(nix): refresh pnpm deps hash
* test(e2e): open settings through the entry settings menu
The home settings entry is now a menu (EntrySettingsMenu): clicking the
gear opens a popover whose "Settings" item opens the full execution-mode
dialog. Update the three specs that assumed a single click opened the
dialog directly to go through the menu trigger + open-details item.
* test(web): drive HomeView context picker through the Lexical helper
The home hero input is a Lexical contenteditable, so fireEvent.change /
reading .value throws "element does not have a value setter". Switch the
MCP+connector first-turn-context spec to setHomeHeroPrompt / homeHeroPromptText
like the rest of the file already does.
* Fix PR review blockers
* Update Nix pnpm deps hash
* Enhance project folder routes with error handling and tests
- Added checks for project existence in GET, POST, and DELETE folder routes, returning a 404 error if the project is not found.
- Updated tests to verify 404 responses for unknown project IDs in folder operations.
- Improved folder metadata handling in project routes.
- Refactored the TabLauncherMenu component for better UI structure and scrolling behavior.
- Adjusted styles for the TabLauncherMenu to improve usability and visual consistency.
* Update ProjectView component and enhance design toolbox localization
- Modified the ProjectView component to include workspacePanelTrack in chat panel width adjustments.
- Updated localization files to add new keys and translations for the design toolbox in English, Simplified Chinese, and Traditional Chinese.
- Enhanced user experience by providing comprehensive tooltips and prompts for design toolbox actions.
* Add browser use prompt launcher
* Cover browser use prompt launcher
* Add design toolbox badge translations
* Refactor ChatComposer and DesignBrowserPanel components
- Removed the design system picker from the ChatComposer component and integrated it into the StagedRunContexts for better context handling.
- Updated the DesignBrowserPanel to include a new function for determining viewport icons and modified the browser use categories to reflect changes in action prompts and titles.
- Enhanced the HandoffButton component with a new feature to copy the project path to the clipboard, improving user experience.
- Added new styles for search input and empty states in design files, enhancing the UI consistency across components.
* Enhance ChatPane and DesignBrowserPanel components with new features and styles
- Added scroll handling features to the ChatPane component, including scrollable state management and improved user experience during chat interactions.
- Updated the DesignBrowserPanel to include new localization keys and improved category titles for better clarity in the browser use prompt.
- Enhanced styles for chat log scrolling behavior, providing a more intuitive interface for users.
- Implemented keyboard shortcuts for tab navigation in the WorkspaceTabsBar, improving accessibility and user efficiency.
* Fix web workspace test hang
* Update styles and functionality for workspace tabs and chat components
- Adjusted CSS for the MAC_WINDOW_CHROME to improve spacing and margins for better layout.
- Enhanced the ChatComposer component to ensure proper tab order and mention handling, improving user experience.
- Implemented keyboard shortcuts for navigating workspace tabs, allowing for more efficient tab management.
- Updated localization strings for clarity in search prompts across multiple languages.
- Improved the HandoffButton component's UI for better visibility and interaction when copying project paths.
* Update visual settings flow
* Refresh Nix pnpm deps hashes
---------
Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>
Co-authored-by: qiongyu1999 <2694684348@qq.com>
Co-authored-by: Siri-Ray <2667192167@qq.com>
* fix(web): preserve preview scroll across tools
Capture URL-loaded preview scroll state before tool handoff and restore it through an opt-in raw HTML bridge to avoid jumping back to the top.
Agent-Model: gpt-5
Agent-Family: openai
Agent-Session: 019e6ceb-c33d-7cd3-bff0-cbc20c642197
Agent-Step: 0.0.6
* test(daemon): cover scroll bridge injection paths
Agent-Model: gpt-5
Agent-Family: openai
Agent-Session: 019e6ceb-c33d-7cd3-bff0-cbc20c642197
Agent-Step: 0.0.6
---------
Co-authored-by: Codex <gpt-5@openai.com>
* feat(daemon): HTTP 206 range request support for video/audio files (#784)
Stream video and audio via fs.createReadStream with Accept-Ranges: bytes
and 206 Partial Content responses so browsers can play and seek media
inline. Non-media files keep the existing buffer path unchanged.
Add parseByteRange (RFC 7233-compliant) and resolveProjectFilePath to
projects.ts, and 23 unit tests covering all range edge cases.
* fix(daemon): move range streaming to /raw/* route used by media viewers
The inline VideoViewer and AudioViewer components fetch
/api/projects/:id/raw/* (via projectRawUrl), not /files/*.
Apply the HTTP 206 / Accept-Ranges streaming path to the raw route
while preserving its Origin: null CORS behaviour for sandboxed iframes.
Add 7 route-level HTTP tests against a real startServer() instance
covering 200 full, 206 partial, suffix range, open-ended range, 416
unsatisfiable, non-media passthrough, and 404 cases.
---------
Co-authored-by: mrcfps <mrc@powerformer.com>