* docs: specify pricing vision model update
* docs: plan pricing vision model update
* docs: record pricing vision usage estimate
* test(pricing): cover vision model entitlement
* feat(pricing): add DeepSeek vision unlimited model
* fix(pricing): fit long vision model name in plan cards
* fix(pricing): widen usage chart label spacing
* feat(pricing): explain model entitlement activation
---------
Co-authored-by: zhanghuihua <zhanghuihua@zhanghuihuadeMacBook-Pro.local>
* ci: surface merge-queue CI failure ejections as a PR comment
A failed workload in a merge_group run ejects the queued PR just as
invisibly as a blocking label does: the required check fails on the
queue's transient ref, the PR's own checks stay green, and the queue
entry silently disappears. The needs-validation gate (#5519) announces
its own ejections, but every other ejection left no trace on the PR.
The validate job now produces a handoff/comment artifact after the gate
has already failed in a merge group, naming the failed jobs and steps, a
log excerpt of the failing assertions, the run, and the PRs batched ahead
in the group. comment.atom.yml already consumes merge_group handoffs.
merge_policy exposes whether it already announced a label ejection so a
label-only ejection still gets exactly one comment.
* ci: keep the merge-queue log excerpt working on gh >= 2.97
gh 2.97.0 (GHSA-3m3g-3wcr-px46) refuses to print an API body that contains
terminal escape sequences unless --allow-escape-sequences is passed, and
CI job logs are full of them, so the excerpt fetch returned nothing on a
current gh. Older gh rejects the flag as unknown. Probe the flag once and
pass it only when supported; the fake gh in the topology test now enforces
both behaviors so dropping the probe goes red.
* ci: make the merge-queue notice probe survive pipefail and replays
Capturing gh api --help before grepping keeps an early grep exit from
SIGPIPE-ing gh and reading as "flag unsupported" under pipefail; strip
CRLF from job logs; never list the gate job itself as a failed job when
a completed run is replayed. The fake gh pads its help text past the pipe
buffer so the old probe shape stays red.
* fix(web): validate onboarding runtime on continue
* chore: ignore local pi state
* refactor(web): remove unused onboarding code
* test(e2e): align onboarding Continue expectations
* fix(web): discard onboarding validation results the user moved past
Continue awaits a validation round trip before it persists the runtime
configuration and finishes onboarding, but its closure is frozen at click
time and nothing rechecked the intent afterwards. Two ways that surfaced:
- Back stays enabled while the request is in flight. Clicking it returned
the user to the model-source chooser, then the late success persisted the
configuration they had just abandoned and completed onboarding under them.
- The inputs being validated stay editable. A success for a replaced BYOK
key still completed onboarding against the click-time configuration.
`inputKey` already guarded what gets DISPLAYED — `visibleAgentTestState` and
`visibleProviderTestState` fall back to idle once it drifts. Apply the same
rule to what gets PERSISTED: `continueAttemptStillCurrent` requires the
attempt to still be on the runtime setup step, on the same runtime, and
against the inputs it validated, reading them from a ref the continuation
can actually ask.
* fix(web): hide the zero balance for subscribed plans
On Go / Plus / Pro / Max the popular models a user works with every day are
unlimited, so the wallet only meters flagship calls and legitimately sits at
$0.00. The workbench's top-right credits pill rendered that zero permanently
next to the avatar, which reads as "you are out of money" to a subscriber who
is not.
`shouldShowCreditsBalance` hides the pill for a subscribed tier whose balance
is exactly zero. Three cases deliberately keep it: a free plan (where zero is
the number that explains why hosted models are unavailable), an unresolved
plan (so the pill cannot flicker in and out as billing lands), and an
overdrawn wallet (the one case the user has to act on).
The tier table it asks lives in `state/plan-unlimited-models.ts` — the same
per-tier unlimited sets the model badge uses — so "which plans may hide their
zero" and "which models are unlimited" cannot drift apart.
* fix(web): badge unlimited models by plan tier, not just the campaign
The 「无限使用」 tag in the model chip and the compact model list was wired to
the DeepSeek V4 campaign alone, so it marked exactly two models for everyone.
What the product actually sells is an unlimited SET PER TIER — 3 models on Go,
4 on Plus, 5 on Pro, all 8 on Max — so a Pro subscriber saw no tag on Kimi
K2.7 Code (unlimited on their plan) while the metered MiniMax M2.7 sat right
next to it looking identical.
The badge now has two sources and the campaign stays first: inside the
campaign window DeepSeek V4 Pro / V4 Flash keep the campaign copy and tooltip
for everyone, free plans included. Only when the model is not a campaign model
does the badge ask the subscription, via `isUnlimitedModelForPlanTier`. BYOK
execution claims nothing — those calls are billed by the user's own provider.
Pricing had the Pro row wrong in the same place: it listed MiniMax M2.7 as
unlimited and GLM-5.2 as metered, the reverse of what Pro includes. Fixed
there too, and `e2e/tests/pricing-unlimited-models.test.ts` now pins the
marketing table and the runtime table together across the app boundary so
editing one side alone fails.
* fix(web): drop the invented tooltip from the plan unlimited badge
The plan branch shipped a tooltip line nobody wrote as product copy (and 19
translations of it). The campaign branch's tooltip is campaign copy with a real
source; the plan case has none, so it now renders the badge text alone rather
than an invented explanation. `od-tooltip` and the aria-label only attach when
there is real copy behind them.
* fix(e2e): type the pricing-parity guard under noUncheckedIndexedAccess
`Typecheck workspaces` failed on the new guard: the e2e package compiles with
noUncheckedIndexedAccess, where every regex capture is `string | undefined`, so
the `match![1]` shape from the first draft does not compile there even though
apps/web accepts it.
Capture access now goes through `captureOne` / `captureAll` / `tierEntry`,
which throw a message naming what could not be found instead of asserting
non-null — so a renamed constant fails as "unlimitedByTier not found — did it
get renamed?" rather than as a TypeError.
* fix(web): reuse the shipped Unlimited wording in ru/uk
The plan badge's Russian and Ukrainian values were written from scratch
("Без лимитов" / "Без обмежень") while the campaign badge beside them — the
same word, already shipped and translated — says "Безлимит" / "Безліміт".
Two different words for one state in the same popover is a translation bug,
so the badge now reuses the shipped wording. The other 17 locales already
matched.
* refactor(web): fold the badge's plan table into the one #7187 landed
#7187 shipped `runtime/amr-unlimited-models.ts` — the same per-tier unlimited
sets this branch had added under `state/plan-unlimited-models.ts` — while this
branch was open. Two copies of a product promise is exactly the drift the
cross-app guard exists to prevent, so the duplicate is gone and everything
reads the surviving table.
The badge's extra needs moved into it rather than staying in a second file:
`planUnlimitedTier` reads the tier out of a team-namespaced id (`team_pro`,
`team_max_yearly`) and `isUnlimitedModelForPlanTier` compares the model slug
past any provider prefix. `isUnlimitedAmrModelForPlan` is untouched — the
balance preflight matches the personal ladder exactly on purpose, since Team
workspaces spend their own balance (#7187), and folding the two questions
together would have changed that behaviour silently.
`e2e/tests/pricing-unlimited-models.test.ts` now parses the surviving table,
including the `...GO_UNLIMITED_MODELS` spreads it composes tiers with, so the
Pricing page and the runtime stay pinned to each other.
* fix(web): restore Settings About update action row spacing
The About page's update row holds two sibling buttons — the primary
update action and "View release notes". #6142 dropped the
.settings-about-update-actions rule group while SettingsDialog.tsx kept
emitting the class, leaving the container at display: block. JSX puts no
whitespace text node between adjacent elements, so the two inline-flex
buttons rendered with their borders touching at exactly 0px.
Give the container back its flex row and 8px gap. Adds a Playwright UI
regression guard that stubs the packaged-desktop host bridge with a
downloaded update so both buttons render, then asserts the declared gap
and the distance the buttons actually land at.
* fix(web): stop the project frame flashing twice on entry from Home
Submitting from Home hands off through ProjectCreationPendingView while
POST /api/projects settles, then swaps in ProjectView. Both components
owned their own `div.app`, so React mounted a second element on the
hand-off and .app's 180ms od-fade-in entrance restarted from zero. The
pending surface only lives ~150ms — shorter than the animation it starts
— so the frame never finished its first fade before the second one
began, which reads as the project view flashing twice.
Move the .app shell up to App.tsx and render it around both branches.
React now reconciles one element across the hand-off: the entrance plays
once, continuously, while the contents swap underneath.
Measured in a real browser on the Home -> project transition:
before t=260 .app#1 od-fade-in@0 -> t=408 .app#2 od-fade-in@0 (restart)
after t=200 .app#1 od-fade-in@0 -> t=317 .app#1 od-fade-in@117 (continuous)
* feat(web): add Go campaign entries and route upgrades to pricing
* fix(web): localize Go campaign copy
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* feat(web): match Go campaign demo UI
* test(web): align Go campaign source contract
* fix(web): align Go modal model icons
* fix(web): preserve Z.ai mark in Go modal list
* fix(web): localize and fit Go campaign modal
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* fix(web): clamp Go campaign modal height
* fix(web): use local Go campaign icons
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* test(e2e): stabilize Go campaign viewport assertion
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* fix(web): keep first Home run output live
* test(e2e): bound design system restoration probe
* fix(web): preserve live turn through authority reload
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
The production landing-page promotion pinned DSH_BOOTSTRAP_VERSION: v1 in
the workflow, while the publisher refuses to overwrite an already-published
immutable version with different bytes. PR #6983 renamed the brand inside
install-dsh.ps1 and install-dsh.sh without bumping that pin, so the next
production deploy failed closed on
immutable bootstrap object already exists with different content:
bootstrap/dsh/v1/install-dsh.ps1
and skipped every later step, including the Cloudflare Pages deploy. The
version is a function of the installer bytes, so maintaining it as a
hand-written constant guarantees this drift.
The publisher now resolves the version itself: it probes published versions
in order using SHA256SUMS as the fingerprint for the whole set, reuses the
version that already holds these exact bytes, and mints the next one when
the bytes changed. Every published version keeps if-none-match: * and the
immutable cache header, so history stays permanent and unoverwritable --
only the manual bump is gone. A mutable bootstrap/dsh/latest.json pointer
records the current version, per-file sha256, and the publishing run, and
the verify step reads the resolved version instead of naming one.
DSH_BOOTSTRAP_VERSION survives as an explicit escape hatch: setting it
forces one version and still fails closed on a content mismatch.
* fix(web): stabilize home examples during startup
Keep the Home examples rail at stable geometry while Workspace discovery resolves, and reveal only the settled Workspace-scoped plugin catalog.
* fix(web): preserve Home example geometry while plugins load
Keep loading and settled Home example cards on one responsive size contract so plugin discovery cannot move the centered startup layout.
Add browser geometry coverage at desktop, tablet, and narrow viewports.
---------
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
* chore: unify product name display to OpenDesign repo-wide
Rename the user-facing product name from "Open Design" to "OpenDesign"
across documentation, web/desktop UI strings, i18n locales, prompts,
plugin/skill/design-system content, changelogs, and specs, matching the
landing-page rename already in flight.
Deliberately excluded so installed-app identity stays untouched:
- packages/release PRODUCT_NAME and everything derived from it
(app bundle names, installer/registry identity, updater feeds)
- tools/pack, tools/release, apps/packaged, landing page (PR #6983)
- mocks/ recorded traces and LICENSE
- test fixtures and comments that encode real on-disk install paths
(Open Design.app, Program Files\Open Design, AppData paths)
Validated with pnpm guard, pnpm typecheck, and targeted web/desktop/host
test runs over the renamed strings.
* fix(web): keep two-letter author monogram for CamelCase brand names
authorInitials split words on whitespace only, so the renamed single-token
author name "OpenDesign" produced a one-letter monogram. Treat lower-to-upper
CamelCase boundaries as word breaks so the avatar fallback stays "OD".
* fix(release): restore spaced Windows source artifact names in win.ps1
tools/pack still derives builder outputs from the unchanged PRODUCT_NAME
("Open Design"), so the asset script must look for the spaced filenames.
Matches the mac/mac-intel/linux asset scripts, which were already kept on
the original names.
---------
Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
* feat(export): add standalone HTML export
Bundle project-local HTML, CSS, JavaScript, images, fonts, workers,
and iframe dependencies into one offline-safe file.
Expose the same contract through the web download flow and od CLI,
with structured failures and bounded output assembly.
* fix(daemon): honor standalone source URL semantics
Resolve document assets against local base href values, reject browser-invalid TypeScript and JSX sources with a structured 422, and count unquoted first-level resources before graph traversal.
Validated with daemon tests, workspace typecheck, and guard.
* fix(daemon): handle standalone data scripts and CSS URL tokens
Preserve non-JavaScript script payloads as data blocks and scan CSS values without treating strings or comments as project dependencies.
Validated with focused and full daemon tests, workspace typecheck, and guard.
* fix(daemon): classify standalone export resources correctly
Preserve non-HTML iframe MIME types, inline only resource-bearing link relations, and propagate unexpected Vite dist read failures instead of silently falling back.
Validated with focused export tests, the full daemon suite, guard, and workspace typecheck.
* fix(export): preserve version and stylesheet semantics
Hide standalone HTML export for historical versions until dependency snapshots are available, avoiding an action that always fails.
Keep disabled and alternate stylesheets as embedded link elements so standalone export preserves activation semantics. Validated with focused red/green coverage, the full web suite, pnpm guard, and pnpm typecheck.
---------
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
* feat(campaign): extend the DeepSeek V4 campaign to Pro + Flash
The V4 Flash free week ends 8/13 20:00; a two-week window offering BOTH
V4 Pro and V4 Flash opens at that same instant and runs to 8/27 20:00.
This grows the campaign already in production rather than landing a
second stack beside it. That implementation ran the free week for a
week: its audience resolution (wallet balance is not a subscription),
i18n wiring, localized countdown, frequency control and attribution are
all already exercised, and it carries a product decision this change
must not undo -- the URL review backdoors (campaign / audience / usage
overrides) were deliberately removed, and reviewing the campaign is done
by overriding `window.startAt`, never through query parameters.
Almost all of it is the campaign module growing a second model:
`isDeepSeekV4FlashCampaignModel` now matches either id, which is why the
model switcher badges both without its rendering changing at all, and
the modal's benefit line is an i18n key, so naming both models was copy.
`「立即使用」` now switches a paid user to Pro, the headline benefit.
Two things needed real edits. Model-benefit impressions fired once with
a hardcoded Flash id; product compares the two models' reach separately,
so they now fire once per campaign model actually on screen, each with
its own `model_id`. And `campaign_id` / `element` move to the spec's
`deepseek_v4_pro` naming, with `deepseek_v4_flash` kept declared so the
finished campaign's warehouse rows stay valid.
Copy is rewritten across all 19 locales -- dates, the two-week framing,
both model names, and the peak-hours queue caveat product added. Dates
follow each locale's existing format rather than one imposed format, so
only the day numbers move.
Frequency control is keyed on the campaign id, and the id is new, so a
user who dismissed the free week is still owed this campaign's single
showing. That was an invisible dependency on remembering to change the
id; it now has a test.
Module and i18n key names keep their `…V4Flash` spelling: renaming 28
keys across 19 locales mid-campaign is risk without user-visible
benefit, and belongs in a cleanup pass once the campaign is off.
* fix(web): read a rolling model window as a wait, not a failure
vela's hosted gateway rejects a request that exhausted its rolling
per-model window with `model_limit_exceeded`, phrased for API callers as
"You have reached the 5-hour usage limit for X. Try again after <instant>.
This request was not charged to Wallet Credits."
Two things went wrong with that sentence on the way to a user.
The daemon's `isHardQuotaText` matches its "usage limit" wording, so a
window that resets on its own was classified as `hard_quota` — an
exhausted quota, marked non-retryable. Nothing was charged and nothing
needs topping up, so that both misnames the cause and counts a self-
healing wait against reliability as a real failure.
The web runtime then had nowhere to put it. The AMR branch of
`resolveRunFailureUi` ends in a catch-all returning the generic "task
failed" title with a null message key, which renders the raw upstream
string — so a localized chat showed an English sentence under a wrong
heading. Home is worse: its send path fails before a run exists, never
reaches that mapping at all, and prints `err.message` verbatim.
Classify the window separately, ahead of the hard-quota reading, and give
both surfaces the same localized copy: high demand, when it reopens, and
the reassurance that nothing was charged. The instant is rendered in the
reader's locale — the gateway reports UTC. Both surfaces read the signal
through one shared contracts module so they cannot drift, and degrade to
copy that does not promise a time when no instant is readable.
Other rate limits are untouched: a genuine session-limit exhaustion keeps
its hard_quota reading, and every other business failure on Home still
prints verbatim.
* fix(analytics): keep a campaign entry for the whole campaign
The attribution record expires after seven days, but this campaign runs
fourteen. A visitor who clicks the banner on day 1 and subscribes on day
11 arrives at checkout with no entry left to attribute the payment to,
so the campaign under-reports against the metric it is judged on --
活动归因付费人数 / 金额, both defined as payments carrying its
`campaign_id`.
Extended only for entries that carry a `campaignId`. Raising the window
globally would move the attribution口径 for every other entry point and
the dashboards already built on it; the campaign is the thing that needs
a longer memory, so it is the thing that gets one. Past fourteen days a
campaign entry expires like any other -- the longer window is scoped to
the campaign, not an open-ended exemption.
* fix(campaign): carry the new campaign id through ingest and e2e fixtures
Two places still recognised only the finished free week's id, both found
in review.
The daemon's AMR entry ingest allowlist is fail-closed: an unrecognised
`campaignId` voids the WHOLE entry rather than just its campaign field.
With only `deepseek_v4_flash` listed, every entry this campaign produces
would have been dropped at ingest -- and its success metrics are defined
as payments carrying its `campaign_id`, so it would have reported zero
conversions while converting normally. Both ids are listed now, since
entries minted during the free week can still arrive inside their
attribution window.
The Playwright suite pre-seeds a dismissal so the campaign modal never
opens over a functional spec. That seeding was keyed on the old campaign
id, so the modal would have covered Home in every UI spec. `home-hero-
rail` also clears storage and re-seeded its own hardcoded copy of the
key; it now reapplies the shared map instead, so a future campaign
cannot be dismissed in one place and left interrupting specs in the
other.
* fix(campaign): land the workbench badge on the console, like the modal
The badge opened a hardcoded `https://open-design.ai/zh/pricing/` while
the modal's CTA opened the console's plan surface scoped to the caller's
workspace. Both are in-product entry points for a signed-in user, so one
funnel was being split across two destinations -- and only one of them
lets the user actually subscribe.
The hardcoded link also pinned `/zh/`, so every non-Chinese user landed
on a Chinese page from a badge that renders in all 19 locales.
Now shares the modal's destination, including the workspace scope, so
the console can resolve the right upgrade surface for that workspace.
Verified against a real browser: the badge opens
`/amr/dashboard?workspaceId=…&billing=plan` carrying entry id, source,
campaign id, conversion source and device id.
* feat(web): home type-fan picker, floating account cluster, docked tab dropdown
Home hero: the 12 create-scenario types render as a fanned card carousel
above the composer (flat elliptical arc, apex card = selected, arrows /
card click switch along the arc; selected card scales about its own
center on an inner body so the outer transform stays a pure arc move).
Entry chrome: the account module leaves the nav-rail bottom and rides a
fixed top-right cluster next to the DeepSeek campaign badge — credits as
its own pill, avatar-only circular trigger, hover menu opens downward
(right-aligned, width-capped). The cluster is app-region no-drag so the
menu stays reachable in the desktop shell.
Project route: the docked workspace tab strip becomes one white dropdown
spanning the chat column (MRU order, most recent first), and the chat
collapse control lifts out of the chat card header to sit left of it.
Design files: the category tab bar drops its near-opaque sticky backdrop
strip and scrolls with content.
* feat(web): polish top-right capsule and dock controls, seed default deck chip
- Home composer seeds 幻灯片 (deck) as the default creation type on a
fresh mount — same silent deferred-apply path as a user pick, skipped
whenever a persisted chip draft owns the composer.
- Type fan: tighter card spacing (arc pivot 3700px → 3000px, ~128px
between neighbours) so the hand-of-cards overlap reads again.
- Top-right chrome: credits + avatar merge into ONE capsule (pill
material on the shared wrapper, chrome-free 24px segments inside,
no hover fill on the credits segment).
- Project dock: the chat-collapse control gains a white seat matching
the tab dropdown, and the dropdown drops its border (shadow only).
* test(web): align refreshed home chrome acceptance coverage
- Read account badges and member labels from the floating account menu.
- Match the default deck route, non-sticky Design Files tabs, and lifted chat control.
- Navigate projects through the visible docked Home control instead of hidden state nodes.
- Cover the affected Web suite and focused UI P0 restoration flows.
* fix(web): lock submit until default routing is ready
- Keep fresh-home sends disabled while the plugin catalog and default deck binding settle.
- Let explicit plugin handoffs supersede the default without leaving Send locked.
- Add a red-spec that holds the catalog response and verifies the routing barrier.
* test(e2e): await seeded type before picker actions
- Wait for the fresh-home deck binding before opening the creation-type menu.
- Switch to Prototype instead of clicking the already-active deck row.
- Preserve clear-selection coverage without racing menu reconciliation.
* fix(web): keep type menu open during internal scroll
- Listen for scroll only on the trigger ancestor chain and the viewport.
- Let the portaled creation-type list scroll without dismissing itself.
- Add coverage that distinguishes internal menu scroll from anchor-moving scroll.
* test(web): await routed submit readiness in legacy flows
- Wait for the Home submit control to become enabled before exercising the AMR workspace race path.
- Wait for refreshed workspace metadata and default routing to settle before submitting a community template.
- Keep the assertions aligned with the production guard that blocks submits until a deterministic default route exists.
* feat(web): replace the home type fan with a capsule pill row
The 12 create-scenario types render as ONE line of capsule pills exactly
as wide as the composer below: pills that fit render inline (8px gaps
throughout), 视频 stays pinned before the 全部 trigger, and the rest fold
into the 全部 popover. Fit is measured against an invisible probe row so
visibility never feeds back into its own measurement. 幻灯片 leads the
order (default-selected type); 网站复刻 trails so it folds first.
The fanned card carousel this replaces is fully removed (component +
styles) — selection still flows through the same onPick scenario path.
* feat(web): drop the clear-type control, cap the type menu at 6 rows
Clearing the creation type is gone (per product): the composer pill loses
its inline x (the chevron now stays in every state) and the menu loses its
leading Clear row, so a type is only ever swapped for another. TemplatePicker
no longer takes onClear.
The type menu shows exactly 6 rows and scrolls for the rest (6 x 44px row +
5 x 2px gap + 12px padding).
Specs that drove the removed control are rewritten around swapping instead:
the e2e clearHomeTemplate helper is deleted, pickHomeTemplate now observes
the pill's has-selection state, and the three clear-specific specs assert the
affordance is absent.
* feat(web): lift the GitHub star badge into the top-right cluster
The star count leaves the account menu's social row and becomes its own
chip between the campaign badge and the account capsule, sharing their
pill material. The menu's social row keeps Discord / X / mail.
* chore(web): clean up stacked EOF whitespace
- Remove the trailing blank line introduced in the home hero stylesheet.
- Keep the shared Playwright home helper free of the same EOF artifact.
* fix(web): polish preview workspace controls
* fix(web): stabilize floating comment interactions
- Treat a configured comment portal as an overlay from the first render and
pre-mount its empty host, preventing the preview from briefly entering the
320px local side-dock layout before the floating card appears.
- Route the floating card's close button and Escape key through one cleanup
path, restore focus to the originating toolbar trigger, and clear board,
create, and composer state so the card reopens with one click.
- Add red-spec coverage for the transient side-dock class, single-action
close/reopen behavior, and focus restoration; the tests fail on the PR base
and pass with this fix.
- Align the signed-out balance action with pill-radius controls, remove the
redundant portal label, delete obsolete radial/reset/search/card picker CSS,
and clean the two stacked EOF whitespace issues.
- Preserve the intended steady-state overlay behavior and validate the change
with 389 focused tests, workspace typecheck, guard, diff checks, and Electron
frame/focus sampling.
* test(e2e): align shared project title assertion with focus mode
- Wait for the file workspace to become visible after first-open materialization.
- Assert the canonical project-title state without requiring the intentionally hidden focus-mode dock to be visible.
- Preserve stale-title rejection and the visible catalog-title check after returning to All Projects.
- Validate with three focused repetitions, guard, workspace typecheck, and the entry-settings CI lane.
* fix(web): stabilize responsive home controls
- Recompute the Home type-pill split when localized labels or intrinsic pill widths change without a container resize.
- Observe the measured pill children and tail so late font layout cannot leave the overflow decision stale.
- Restore composer errors to the reviewed in-flow presentation instead of carrying the unrelated fixed top-chrome toast.
- Add focused coverage for locale-style rerenders and post-render width settling.
* test(e2e): await default home routing in attachment flow
- Wait for the visible deck binding before checking the Home attachment submit lifecycle.
- Use the shared long timeout so grouped CI does not race catalogue readiness.
- Preserve the production submit gate and the attachment add/remove assertions.
* feat(web): add a view-all-comments entry to the comment popover
* feat(web): cap the docked tab dropdown at six scrollable rows
* fix(web): preserve comment and tab menu interaction semantics
- Return focus to the still-open View all comments control when the floating comment panel closes.
- Include border-box borders in the six-row tab menu height so the last row is not clipped.
- Add focused regression coverage for comment focus restoration and menu sizing.
* test(web): await default routing in AMR home flows
- Wait for the cold Home composer to finish binding its default deck route before clicking Send.
- Preserve the existing AMR balance retry and auth-expiry assertions after the new readiness gate.
* fix(web): preserve explicit Home submit routes
* fix(web): defer restored Home plugin apply
* fix(web): close workspace chrome review gaps
- Clamp the Home template menu to six rows and the visible viewport, flipping it above the trigger when needed.
- Reset docked tab-menu state when its route-owned portal disappears and preserve editor-owned keyboard shortcuts.
- Scope the project header account identity, balance, and billing destination to the project workspace while retaining the ambient fallback for unbound local projects.
- Cover tall and short viewports, dock remounts, composer focus, and ambient/project workspace divergence.
---------
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: Amy <1184569493@qq.com>
* test(e2e): cover shared design system logo assets
* test(e2e): reuse team design system collaboration cluster
* test(e2e): select team workspace before owner logo check
* test(e2e): activate owner team workspace before logo checks
* test(e2e): wait through workspace tab restoration
* fix(daemon): point users at the Settings section that actually exists
When image generation failed for a missing credential, the reply told
the user to configure it in "Settings → Media". No such nav item exists —
the section is "Media providers" ("媒体生成提供商" in zh-CN) — so users
following the instruction found nothing.
The cause is that Settings destinations were spelled independently
wherever a producer happened to need one: 29 provider errors in
apps/daemon/src/media/index.ts said a bare unnamed "Settings" (one said
"Settings -> Media Providers"), and the od-media-generation skill said
"Settings → Media". None of them were tied to the Settings dialog, so a
rename could not reach them.
packages/contracts/src/settings-nav.ts is now the single place a Settings
destination is spelled, and every producer reads it: the media provider
errors, the ElevenLabs voice-options prompt (daemon + contracts), and the
external-MCP auth guidance. e2e/tests/settings-nav-copy.test.ts holds the
constants against the labels apps/web actually renders and fails the
build if a producer re-inlines its own guess.
Swept the same drift class while here: both hatch-pet SKILL.md copies
pointed at "Settings → Pets", but #5517 folded the pet picker into
General, so the real path is "Settings → General → Pets".
Verified the gate red on the pre-fix tree (3 failing assertions) and
green after. Adjacent, not fixed: these strings are English and the
agent translates them ad hoc, so a localized client can still render a
section name that does not match its own UI — see the report.
Record: recvre8FrTE2Oa (V0.19.1 acceptance)
* fix(contracts): gate destinations on rendered nav, not the i18n dictionary
Review on #6831 caught that SETTINGS_EXTERNAL_MCP named a place users
cannot navigate to. `mcpClient` kept its Settings render block but lost
its sidebar nav item (SettingsDialog.tsx says so in its own comment), and
reconnecting an external MCP server now lives in the top-level
Integrations view. So the destination is not a Settings section at all.
The stale string predates this branch — it was hardcoded in
prompts/system.ts on main. But promoting it into a named constant made it
worse twice over: the docblock asserted "The Settings nav item where
external MCP servers are added/reconnected" in a canonical location, and
the new gate whitelisted it by construction, so the check built to catch
dead paths became the thing defending one.
The deeper defect was the gate's invariant. Comparing a constant to its
i18n key's value proves only that some dictionary entry matches — not
that anything renders it. `settings.externalMcpTitle` still resolves to
"External MCP" with no nav item behind it, which is exactly how the dead
path passed. The same hole covered SETTINGS_MEDIA_PROVIDERS.
Now the gate parses what the UI actually renders — `settings-nav-item`
buttons and INTEGRATION_TABS — and resolves those keys through the
dictionary, using it as a lookup rather than as the source of truth.
Verified by reintroducing "Settings → External MCP": the old gate passed
it, the new one fails with the rendered destinations listed.
Replaces SETTINGS_EXTERNAL_MCP with INTEGRATIONS_MCP_PATH
(`Integrations → MCP`) and adds a regression test pinning that a
dictionary-only label is rejected.