Keep local daemon and web validation scoped to affected files and integration boundaries. Reserve full suites for PR CI unless the user explicitly requests them.
Adapt model and authentication probe regressions to the marker-based Codex fixtures without restoring plan templates or snapshot helpers.
Validated the affected daemon server file (56 tests), web refresh recovery file (61 tests), workspace typecheck, and guard.
* fix(web): reveal full model names on hover in model pickers
Long model ids in the model pickers ellipsise to a shared prefix (e.g.
openrouter/google/gemini-...), so rows become indistinguishable and no title
was available to confirm a choice. Add the full label as the hover title on
every model-selection surface named in the report:
- the shared SearchableModelSelect option rows and trigger;
- the compact home list in InlineModelSwitcher;
- the composer/sidebar AvatarMenu lists (daemon and BYOK).
A locked row keeps its upgrade reason as its title, and accessible names are
unchanged. Adds regression tests using two same-prefix long OpenRouter ids on
each surface.
Fixes#8407
* chore(ci): retrigger web workspace tests after flaky artifact-tab shard
The ProjectView.auto-open-turn-artifacts spec timed out on shard 2/2; it
passes locally and is unrelated to the model-picker title change.
Electron net.fetch reports ENOBUFS as net::ERR_NO_BUFFER_SPACE. The existing
guard only matched the POSIX name, so the proxy fell through to undici and
kept retrying on an already-exhausted T2 Intel Mac.
Co-authored-by: leoric <leoricchen@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: remove duplicate web-clone chip in HOME_HERO_CHIPS
HOME_HERO_CHIPS defined two objects with id: 'web-clone'. They were identical
except the second lacked automaticDefault: true. A duplicate id collides as a
React key and makes findChip/orderedCreateChips lookups ambiguous, and the
second copy silently dropped the automatic-default binding. Remove the second
duplicate, keeping the canonical entry that carries automaticDefault: true.
* test(home-hero): assert HOME_HERO_CHIPS ids are unique
Existing findChip and rail tests only observe the first match for an id, so
they passed while two web-clone chips were present. Add a catalog-wide
uniqueness invariant that fails on a reused id, so a duplicate cannot silently
return with its React-key and lookup ambiguity.
`agentCliEnv` is validated against a per-agent allowlist on every save, and
pi exposed only `PI_BIN`. `PI_CODING_AGENT_DIR` — pi's own override for its
config directory, default `~/.pi/agent` — was therefore dropped on every save
cycle, leaving no way to isolate a pi config but a wrapper script that sets
the variable and is pointed at by `PI_BIN`.
This extends pi's contract by the one key rather than turning the allowlist
into a pass-through: `PI_CODING_AGENT_DIR` is the same kind of entry
`CLAUDE_CONFIG_DIR` and `CODEX_HOME` already are for their CLIs, and nothing
else about pi's environment exposure changes.
Persistence is half the fix, so the tests follow the value the rest of the
way: one cell writes the config, reads it back and hands
`agentCliEnvForAgent`'s projection to `spawnEnvForAgent`, asserting the
variable arrives in the env pi is launched with; two more pin the launch env
directly, including `~` expansion. A neighbouring key that is NOT part of the
contract (`PI_CODING_AGENT_SESSION_DIR`) is asserted to still be dropped.
* docs(readme): refresh product tour and translations
* docs(readme): keep product tour media under blob limit
* docs(readme): introduce OpenDesign Go plan
---------
Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
The first live dry run failed at the badge preflight: I had written the tag
as v<version>, but prepare-stable.ts publishes open-design-v<version>. A
hand-built prefix reads plausibly and is wrong, so take the tag from
.versionTag in the restored release's own immutable metadata — the value the
publisher recorded — probing the same prefix candidates rollback-latest.ts
probes, and hand it to the badge step as a step output.
The summary checklist had the same wrong shape; it now names the version
rather than constructing a tag. The regression test pins both: the tag must
come from .versionTag, and no step may rebuild one from the version input.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(release): add one-dispatch stable rollback
A stable release publishes its artifacts to an immutable
stable/versions/<version> prefix and then copies the pointer objects into
stable/latest. latest is therefore derived state, and retracting a release is
copying an older version prefix over it again — but until now nothing in the
repo could do that, so a bad stable release had no stop button.
Add `tools-release rollback-latest` plus a release-rollback workflow that
dispatches it. The CLI refuses unless the live pointer still serves the version
the operator typed as from_version, the target published completely (not
partial, not a dry run), every ready platform's manifest and updater feed exist
in the target prefix, and the current installer floor is satisfiable by the
target. It re-resolves control.launcher.version from live channel policy rather
than restoring the snapshot's, records rollback provenance, writes metadata.json
last so latest never advertises a version whose feeds it does not yet serve, and
verifies every object through both the origin and the public URL.
The workflow shares release-stable's publish concurrency group so a rollback
cannot overlap a publish, moves only the GitHub Release "Latest" badge (never a
release body), and ends with a checklist of what a rollback does not reach: the
download page's version-pinned links, the What's New card, the group
announcement, and above all the installs already on the retracted version —
the updater never offers a version that is not strictly newer than the running
one, so this stops a rollout and cannot recall one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(release): group the rollback summary fallback redirects
shellcheck SC2129 — CI runs actionlint with shellcheck wired in, which my
local actionlint could not see without it installed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(release): prove the restored tag has a release before writing
The badge move runs after the R2 write, so a missing GitHub Release for the
restored version would have failed the run with stable/latest already moved.
Check it up front instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(release): pass the stable installer-floor policy into rollback
resolveLauncherVersionFloor() reads process env, and repository variables are
not process env. Without the handoff a rollback resolved no floor at all: it
would strip an active control.launcher.version from the restored metadata and
stop refusing targets below the current minimum — the safeguard the workflow
documents as enforced. Latent today only because no stable floor is set.
release-stable.yml passes the same pair to metadata publish and verify; mirror
it, with a topology assertion so a later edit cannot drop it silently. Also
stamp RELEASE_BRANCH/RELEASE_COMMIT so the rollback provenance is not blank.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(release): require the restored release to be badge-eligible
gh release view succeeds for a draft or a prerelease, but GitHub will not put
the Latest badge on either — so the preflight passed and the edit failed with
the R2 rollback already done, which is the half-complete state the preflight
exists to prevent. Check isDraft and isPrerelease too, and cover the
eligibility guard in the workflow test alongside its ordering.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(diagnostics): collect the AMR run's runtime log records (OPEND-3498)
AMR incidents never carried AMR-side evidence. The collector looked for
OpenCode logs under <dataDir>/amr/opencode-home, but Vela drops the
OPENCODE_TEST_HOME it receives and runs OpenCode under per-conversation homes
in $AMR_HOME/opencode-sessions, so nothing was listed and no note explained
the gap. $AMR_HOME/logs/agent-runtime.jsonl was never listed either.
- Take the incident run's records from agent-runtime.jsonl, plus those of the
OpenCode sessions it owns until another run takes a session over, so other
runs' records never enter the bundle.
- Report a missing runtime log, a missing run id and the not-yet-located
session logs as explicit collection notes, and keep those reasons instead
of a consent verdict.
- Give sources that appear after opting in a boundary at their current size
on start, so a newly listed shared log is not omitted forever.
Locating the per-conversation OpenCode session logs is left for a follow-up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(diagnostics): keep a rotated log's baseline when extending the fence
On Windows the packaged launcher rotates latest.log to previous.log at every
start. extend() saw previous.log as an unbaselined path and recorded a
boundary at its current size, so the rotated log's bytes written after the
real boundary were dropped as consent_boundary. Found in a packaged Windows
run: the daemon log written after opting in was missing from the bundle.
Skip files that share a baselined creation time; the fence decides their
boundary from that baseline. Skipping only defers to apply() and never
admits bytes by itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(diagnostics): keep Windows rotated logs inside the consent boundary (OPEND-3497)
The packaged launcher rotates latest.log to previous.log and immediately
re-creates latest.log. On Windows, file-system tunneling gives the new file
the creation time of the file that previously held the name, so the consent
fence saw a new inode with a pre-consent birthtime and omitted the whole
file as pre_consent_source on every launch after opting in. The rotated
previous.log was omitted too because its baseline was keyed by the old path.
Match baselined identities across paths so a rotated file keeps its offset,
and admit a new identity whose creation time equals a baselined one, which
only a re-created (tunneled) name produces. Unrelated old files are still
omitted, and no pre-consent bytes are backfilled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(diagnostics): scope tunneled log admission to its source path
Preserve original consent offsets and persist admitted identities for later rotations and daemon restarts. Cover unrelated birth-time collisions, rotation ordering, and consent resets.
Generated-By: looper 0.13.0 (runner=fixer, agent=codex)
* fix(diagnostics): remember tunneled logs at startup, keyed by identity
A session that recorded no incident never called apply(), so its tunneled
latest.log was not remembered as admitted. After the next launch rotated it
to previous.log, that incident-time log was omitted as pre_consent_source,
which is the relaunch-after-crash case. Admitted identities were also keyed
by path, so a newer latest.log replaced the entry of the file that had just
rotated to previous.log.
Observe the baseline sources once per daemon process, after the launcher has
rotated, and key admitted identities by inode and creation time, keeping only
those still present. Verified on Windows (NTFS, default tunneling) with real
rotations: with no incident in the middle session, previous.log is admitted
on the next launch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(diagnostics): report a missing source as not found, not pre-consent
The fence labelled a source that does not exist as pre_consent_source, which
reads as a consent decision. A packaged Windows bundle showed
logs/desktop/renderer.log this way although the file was never created.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(diagnostics): stop repeating API failures from flooding automatic uploads
Stable 0.24.1 devices uploaded thousands of diagnostic incidents a day from
one deterministic failure: GET /api/runs answering 400 PROJECT_SCOPE_REQUIRED.
- Key API-failure incidents by failure signature per hour instead of the
per-request id, so a polling caller yields one incident per hour.
- Drop earlier "[diagnostics] incident delivered" receipt lines from
automatic bundles; on busy devices they filled the daemon log tail.
- Desktop pet task center: stop interval polling once the daemon requires a
project scope, and stop restarting the poller on every 1.5s config reload.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(diagnostics): filter receipts before allocating the log tail budget
Scan automatic text logs backward with bounded buffers, preserving complete lines and consent offsets before applying the retained byte limit. Cover receipt suffixes larger than 4 MiB and filtered-tail boundary cases.
Generated-By: looper 0.13.0 (runner=fixer, agent=codex)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Treat a previous task this build cannot read as absent so the follow-up starts a new task, and stop counting a Run with a pending cancel as holding its conversation. Add end-to-end coverage for the marker continuation flow and restore the remote Workspace directory gate cases.
* feat(billing): support Coding Plan usage and wallet fallback
* fix(web): keep AMR send off billing preflight
Workspace send no longer waits on advisory vela billing preflight; recovery still requests it. Realign zero-wallet P0 oracles and the credits-card wallet label.
Generated-By: looper 0.15.0 (runner=fixer, agent=omp)
* fix(billing): serialize recovery preflight and bound hung vela
Skip overlapping AmrBalanceDialog recovery ticks and stop the watch after
ten minutes even if a preflight is still pending. Give vela billing
preflight a 60s runVelaCommand timeout so a hung CLI cannot pin recovery.
Generated-By: looper 0.15.0 (runner=fixer, agent=omp)
* fix(web): restore coding plan text contrast in billing card
* feat(web): render Coding Plan windows and Go tier in the credits panel
- CodingPlanUsage renders one row per preflight window (name, used %,
remaining USD, reset countdown + absolute time) with loading/unavailable
states; refreshes once on mount and once at resetsAt (no 30s polling,
setTimeout delay clamped to the 32-bit limit).
- Pure model in coding-plan-usage-model.ts; creditsPerUsd comes from
`vela billing summary` through WorkspaceBillingSummary and falls back to
10,000 only when the backend omits it.
- PlanWordmark/EntryNavRail accept the `go` tier (placeholder wordmark,
design asset still pending).
- i18n: 7 new billing keys + entry.billingTierGo across all 19 locales;
real translations for the #8339 keys; drop unused codingPlanNone /
codingPlanRemaining.
- CLI wording: unstarted window reads "not started yet".
* build(pack): pin vela-cli 0.1.4-test.0 and gate on billing preflight
The packaged client needs `vela billing preflight` for the Coding Plan
window rows. Bump the bundled CLI to the npm `test` prerelease that ships
it and add a third capability gate in tools-pack: a CLI without the
subcommand prints the parent `billing` help and exits 0, so the gate
checks the help markers (preflight, --workspace-id, --model, --format)
instead of the exit code.
TEMPORARY: replace 0.1.4-test.0 with the stable 0.1.4 once it is
released; do not merge with a `test` prerelease pinned.
* fix(web): make Coding Plan rows legible on the inverted credits card
The credits card is a hard-coded #202020/#fafafa inverted surface, but
CodingPlanUsage.module.css read page-level --text-* / --bg-fill-* tokens,
so on the card the window names rendered #202020 on #202020 (1.00:1) and
the stat/reset lines sat below WCAG AA. Derive every colour from
currentColor instead (name 100%, stat 72%, reset/note 60%, separator ~50%,
track 36%, exhausted = --red mixed toward currentColor); measured in Chrome:
name 15.6:1, stat 8.7:1, reset 6.4:1, track 3.2:1, exhausted 6.5:1.
Keep the stat on one line (nowrap) and let the name ellipsize instead of
wrapping "60% / used" onto two lines; drop the block's 12px side padding so
it aligns with the card's own rows.
Also add a minutes tier to the reset countdown: the last hour before a
window resets used to show no countdown at all. New key
billing.codingPlanResetsInMinutes in all 19 locales.
* feat(web): rebuild the plan panel 1:1 from design PR #8364
Replace the improvised Coding Plan block with the design's panel, measured
against docs/ui-previews/plan-panels/index.html in Chrome (computed styles
and a 4x pixel diff of the Plus specimen):
- Header: tier name + 16px wordmark, 升级 pill (管理 on the top tier).
- One row 「7天额度免费用 · 已用 N% ›」 (7-day window only; rounded percent;
100% when spent, no red state) over a 5px #b1f38b track, then the
separator and the 「钱包余额 · US$x.xx ›」 row. No headings, window names,
remaining USD, reset time, note or unavailable line.
- Team workspaces keep the pre-existing card unchanged (all team_* tiers);
the quota row renders only for personal workspaces.
- Free personal workspace: no quota row, but the header sells 升级 like the
design (a personal workspace needs no billing permission; an account B
reports without a plan upgrades as free).
- Top-right pill shows the wordmark only, no balance.
- Wallet always reads US$ regardless of locale (product ruling 2026-09-23).
- zh-CN tier names follow the design: 免费版 / 标准版 / 专业版 / 旗舰版.
- Drop the dead billing.codingPlan* keys and model helpers; add
billing.codingPlanWeeklyAllowance and entry.creditsManage in 19 locales.
* feat(web): show 管理 on personal Max per design PR #8364
The design's Max card sells 管理 (console auto-recharge settings) where
every other tier sells 升级; product ruled on 2026-09-23 to follow the
design for personal Max too, superseding the earlier "personal tiers are
always upgradeable" ruling. The gate now asks isMaxPlanTier and 管理 wins
over 升级 when both would show.
* feat(web): route the plan card's three entries per product ruling
2026-09-23 ruling on the card's landings: 升级 opens the console's pricing
dialog (billing=plan, unchanged), the allowance row opens the console
dashboard (unchanged), the wallet row asks for the manual top-up dialog
(new billing=recharge intent; B does not honor it yet and degrades to the
dashboard), and 管理 on Max opens the plain dashboard instead of the
auto-recharge dialog.
* fix(web): align personal plan cards with v2 design and scoped loading
* fix(web): center plan wordmarks inside the top-right pill
* fix(web): reuse shared billing cache for plan hover cards
* test(web): match personal billing preflight in avatar fixture
* fix(billing): require usable quota evidence for funding recovery
* test(billing): cover scoped preflight and quota event refresh end to end
* feat(daemon): relay scoped coding plan usage events and health
* fix(billing): refresh quota caches from scoped realtime events
* test(daemon): accept capability query on mock event stream
---------
Co-authored-by: lefarcen <935902669@qq.com>
* fix(packaged): let a macOS headless runtime become the desktop in place
When Codex starts Open Design headlessly through MCP, macOS treats that
windowless process as the running app. Opening the app from the Dock,
Finder, Launchpad or `open -a` then only reactivates it, so no launcher
runs and no window ever appears (OPEND-3426).
On macOS the headless runtime now runs the desktop path with its window
deferred, and restores the window in the same process when the user
opens the app: through `activate`/`open-url` paired with the app becoming
active, or through SHOW from a newer launcher that sees a `restorable`
owner. The daemon, its port and connected MCP clients stay. A new outer
hands headless launches to the active payload and marks the launch as
managed; payloads started by older outers keep today's behavior.
Managed MCP registrations carry absolute sidecar endpoints for every mode
of the namespace, so the MCP process finds the daemon whichever mode owns
it, waits for a running owner instead of reopening it, and cold-starts
only when nothing runs. The daemon refreshes an existing Codex
registration on start and never installs one. A write interrupted by a
service restart now says it may or may not have been applied.
* fix(daemon): scope Codex refresh to its managed install
Require the existing registration's managed discovery marker to match before refreshing it, so local, stable, and prerelease installs cannot take over the global name from one another. Cover the unrelated-owner path without invoking a config mutation.\n\nGenerated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(daemon): prove Codex registration ownership by data root and app
Tighten the ownership check from d2d47a5f8e. Matching the discovery
endpoints alone still let a copy of the app that reuses the installed
namespace take over the registration (the endpoints depend only on
channel, namespace and source), and it could never refresh a
registration written before the managed contract, which has no
discovery value and still names a payload version that cleanup removes.
Read the existing registration and rewrite it only when it serves the
same daemon data root and bootstraps the same installed app; the managed
marker is ignored so a pre-managed registration of this install still
counts. Anything else, including a registration that cannot be read, is
left untouched.
---------
Co-authored-by: Looper <looper@noreply.github.com>
Publish-public and deploy failures were only visible as one opaque bucket
each (`publish_failed`, generic HTTP_400 / Error), so neither PostHog nor the
automatic diagnostics bundles could tell why they failed.
Purely additive — no existing status, error code, event value, trigger or
copy changes:
- daemon: failed publish/unpublish/deploy responses gain an optional
closed-token `failure` { stage, reason, upstreamStatus?, upstreamCode? }.
Vela CLI failures are classified from the CLI's stable stderr contract
(vela-cli 0.0.33–0.1.3); deploy failures separate provider rejection,
unreachable provider, rejected Vercel token, local file and internal
errors. Each failure also writes one structured, text-free log line.
- web: artifact_publish_result / artifact_deploy_result gain optional
failed_stage, failure_reason, upstream_status, upstream_error_code, and
(publish) daemon_error_code. `error_code` keeps its values.
- correlation: each attempt's analytics request id is sent as
x-od-request-id, logged by the daemon, and reported as request_id.
TemplatePicker closed its menu from a passive effect keyed on activeChipId and
disabled. On a loaded machine React runs a commit's passive effects in a later
task, so a click on a trigger that already showed the new type opened the
menu and the stale effect closed it again. Close in the render that
introduces the change instead.
Home also enabled the picker one render before its first-visit default type
(or restored draft / handoff) landed, so a click in that gap opened a menu the
arriving type then closed. Keep the picker disabled until the initial type is
settled.