* 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>
tools/pack
Local packaging control plane for Open Design.
tools-pack is the cross-platform packaging and smoke-lifecycle control plane. The macOS commands include:
tools-pack mac build --to alltools-pack mac build --to app|dmg|ziptools-pack mac build --to all --signedtools-pack mac build --to all --portablefor release artifacts that must not bake local tools-pack runtime pathstools-pack mac installtools-pack mac starttools-pack mac stoptools-pack mac logstools-pack mac uninstalltools-pack mac cleanup
Build artifacts are namespace-scoped under .tmp/tools-pack/out/mac/namespaces/<namespace>/.
Public release bundles keep channel-distinct identities: Open Design.app, Open Design Beta.app,
Open Design Prerelease.app, or Open Design Preview.app. Local tools-pack install adds the developer
namespace so installs can coexist without affecting runtime data/log/cache paths.
Packaged runtime state is namespace-scoped under .tmp/tools-pack/runtime/mac/namespaces/<namespace>/:
- Packaged daemon storage is governed only by the root
AGENTS.mdsection Daemon data directory contract. Before changing or documenting packaged storage propagation, you MUST read that section; this README MUST NOT restate it. logs/contains packaged process logs fordesktop,web, anddaemon.runtime/is the sidecar runtime base used by the packaged desktop/web/daemon process group.cache/is reserved for namespace-local packaged cache state.user-data/is the Electron/ChromiumuserDataroot, withuser-data/session/used forsessionData.
Every packaged root registers the exact five-field sidecar stamp
(channel / namespace / source / mode / app) in its own argv. Direct OS
launches register during packaged startup; tools-pack launches receive the same
stamp through the sidecar launch atomic. No identity file participates in
discovery or ownership.
tools-pack mac stop scans exact argv stamps for desktop, Web, and daemon across
both tools-pack and direct packaged sources, requests private graceful teardown,
waits for every captured generation to exit, and then force-stops only the
still-matching process trees. Cleanup and uninstall refuse to remove artifacts
while any captured generation remains alive.
This keeps stop from killing processes outside the current namespace.
Packaged desktop also writes main-process lifecycle logs to logs/desktop/latest.log so Finder/manual launches are
diagnosable. This log is intentionally scoped to packaged desktop startup/shutdown/process errors and does not capture
web/renderer console output.
The packaged daemon path contract lives only in the root AGENTS.md section
Daemon data directory contract. Before changing or documenting packaged
path propagation, you MUST read that section; this README MUST NOT restate it.
Packaged desktop checks release metadata, verifies the downloaded artifact, and exposes update actions through desktop IPC. Launcher-based builds prefer verified payload activation followed by relaunch; installer replacement remains the fallback for artifact types and older builds that cannot apply a payload in place.
Electron-builder resources live under tools/pack/resources/mac/. The current logo is staged there as the mac icon/DMG
placeholder so future design-provided assets can replace the resource files without changing packaging code.
Local developer artifacts bake the tools-pack namespace runtime root so tools-pack mac start/stop/logs/cleanup can manage
them from the repo. Release artifacts use --portable so the installed app resolves namespace data/log/runtime/user-data
from the user's Electron userData root instead of the build machine's .tmp path.
macOS compatibility notes
tools-pack mac build --portable --to zipis the safest manual-install artifact for Intel Macs. This path was smoke-tested on macOS 12.7.6 Monterey on a 2015 Intel iMac and the app launched successfully from/Applications.- Finder/manual launches on macOS may not inherit your shell-managed
PATH. If packaged Open Design cannot detect agent CLIs that work in Terminal, expose those binaries to the GUI login environment or launch the packaged app from a shell session that already sees them.
Windows
Local lifecycle commands:
tools-pack win build --to dirfor fast unpacked smoke builds.tools-pack win build --to nsisfor installer builds.tools-pack win build --to allfor both outputs.tools-pack win installtools-pack win starttools-pack win inspect --expr "document.title"tools-pack win logstools-pack win stoptools-pack win cleanuptools-pack win listtools-pack win reset
Build artifacts are namespace-scoped under .tmp/tools-pack/out/win/namespaces/<namespace>/.
Packaged runtime state is namespace-scoped under .tmp/tools-pack/runtime/win/namespaces/<namespace>/.
--to dir may point built-app.json at an immutable cached win-unpacked executable while keeping
namespace-local config and runtime paths outside that cache entry.
Linux
Local lifecycle commands:
tools-pack linux build --to all(default; produces AppImage)tools-pack linux build --to appimage(explicit AppImage)tools-pack linux build --to dir(unpacked output for fast iteration)tools-pack linux build --containerized(run electron-builder insideelectronuserland/builder:baseDocker for a wider glibc compatibility target — requires Docker)tools-pack linux build --to all --portable(release artifacts that must not bake local tools-pack runtime paths)tools-pack linux installtools-pack linux install --headless(install the headless launcher script instead of the AppImage)tools-pack linux starttools-pack linux start --headless(start the headless entry — daemon + web, no Electron)tools-pack linux stoptools-pack linux stop --headless(stop a running headless process)tools-pack linux inspect(desktop status, eval, and screenshot for AppImage mode)tools-pack linux inspect --headless(status only)tools-pack linux logstools-pack linux uninstalltools-pack linux uninstall --headlesstools-pack linux cleanuptools-pack linux cleanup --headless
Build artifacts are namespace-scoped under .tmp/tools-pack/out/linux/namespaces/<namespace>/. Packaged logs, sidecar runtime, cache, and Electron user-data are namespace-scoped under the tools-pack runtime root. Daemon storage follows only the root AGENTS.md Daemon data directory contract. Containerized build cache lives under .tmp/tools-pack/.docker-cache/{electron,electron-builder}/.
Local installs use XDG paths:
- AppImage:
~/.local/bin/Open-Design.<namespace>.AppImage - Menu entry:
~/.local/share/applications/open-design-<namespace>.desktop - Icon:
~/.local/share/icons/hicolor/512x512/apps/open-design-<namespace>.png
The <namespace> suffix is unconditional so multiple developer namespaces can coexist on the same desktop. The .desktop file registers the od:// scheme via MimeType=x-scheme-handler/od; and pre-sets OD_PACKAGED_NAMESPACE on the Exec= line so menu launches identify the correct namespace.
Headless mode (--headless)
Headless mode targets environments without a display (WSL2, headless servers, CI) where Electron can't run. If you have a desktop, use the AppImage; if you're SSH'd into a machine or in WSL, use headless.
--headless makes install, start, stop, uninstall, and cleanup operate on the headless entry (@open-design/packaged/dist/headless.mjs) instead of the AppImage. Headless mode runs daemon + web without Electron.
install --headlesswrites a shell launcher at~/.local/bin/open-design-headless-<namespace>that bakes in the namespace and resource paths. The launcher is self-contained, but the assembled app directory at those paths must remain in place — don't move it after install.start --headlesslaunches through the sidecar atomic and waits up to 95s for private status readiness.stop --headlessterminally stops the exactmode=headlessargv stamp. The mode field keeps it disjoint from the AppImage desktop without identity files or public IPC paths.inspect --headlessreturns status only. Eval and screenshot require AppImage mode because there is no Electron renderer in headless mode.uninstall --headlessremoves the headless launcher after a safe stop.cleanup --headlessstops the headless process before removing namespace output/runtime roots.
logs always reads logs/desktop/latest.log regardless of mode, so headless output is visible via tools-pack linux logs.
AppImage launch mode (FUSE caveat)
tools-pack linux start always spawns the AppImage with --appimage-extract-and-run. Smoke testing on Ubuntu 24.04 and Arch Linux showed that direct FUSE-mounted AppImage launches make Node module loads (Express, better-sqlite3, etc.) slow enough that the daemon sidecar consistently failed to clear apps/packaged's 35-second startup timeout. Extract-and-run unpacks the AppImage into /tmp/appimage_extracted_<hex>/ and exec's the inner Electron from there, bypassing FUSE and getting daemon boot in under 5 seconds — roughly an order-of-magnitude improvement.
Implication for end-users: if launching the installed AppImage manually (not via tools-pack linux start), pass --appimage-extract-and-run yourself, or rely on a desktop launcher / appimage-launcher daemon that handles extract-and-run automatically.
Optional system tools
tools-pack linux install and tools-pack linux uninstall invoke update-desktop-database and gtk-update-icon-cache as best-effort post-hooks. Either tool being absent (iconCache: "missing" in the output) is harmless — the icon and menu entry still work, the cache just isn't refreshed. Install via your distro:
- Arch / CachyOS:
sudo pacman -S desktop-file-utils gtk-update-icon-cache - Debian / Ubuntu:
sudo apt install desktop-file-utils gtk-update-icon-cache - Fedora:
sudo dnf install desktop-file-utils gtk-update-icon-cache
libfuse2 is needed for FUSE-mounted AppImage launch (the default mode when running an AppImage directly without --appimage-extract-and-run). tools-pack linux start always uses extract-and-run and bypasses FUSE entirely, so it does not need libfuse2. Most modern distros ship libfuse2 by default; older Ubuntu LTS hosts may need sudo apt install libfuse2t64 (or libfuse2 on pre-24.04).
Sandbox / chrome-sandbox
Electron 41 on Linux requires kernel.unprivileged_userns_clone=1 (default on Arch, Ubuntu 24+, Debian 12+) or AppImage's --no-sandbox fallback. Most modern distros need no extra setup.
Distro compatibility target
AppImages built natively on a rolling distro (e.g., Arch / CachyOS) link against recent glibc and may not run on stable distros (Ubuntu 22.04, Debian 12). Use --containerized to build against the electronuserland/builder:base baseline (Ubuntu 18.04 / glibc 2.27), which is the compatibility target for release AppImages rather than a guarantee for every Linux distribution.
Verified smoke coverage in this repository currently includes:
- Main PR CI: packaged Linux smoke is intentionally outside the main CI gate.
- Release lane: Ubuntu GitHub-hosted runner, containerized AppImage build plus Xvfb AppImage runtime smoke when the Linux release lane is enabled.
- Manual AppImage behavior used to choose
--appimage-extract-and-run: Ubuntu 24.04 and Arch Linux.
Format choice: why AppImage first
Linux desktop apps in this space split across formats: VS Code ships .deb + .rpm + Snap; Discord ships AppImage + .deb; Slack ships .deb + .rpm; Cursor and Obsidian ship AppImage. We start with AppImage because one artifact can cover the widest glibc-compatible target without distro repositories, store packaging, signing infrastructure, or per-format install scripts, and it integrates cleanly with the namespace-scoped install layout. .deb / .rpm / Snap / Flatpak can land incrementally when user demand justifies the extra release ownership.
Out of scope (later phases)
- AppImage signing (
--signed) — deferred pending a GPG key infrastructure decision and a user-facing verification flow design (no ETA). - AppImage auto-update feed (
latest-linux.yml) — the linux electron-builder config has nopublishblock wired, so a generated feed would point users at a feed that never updates. Tracked alongside signing. - Additional package formats:
.deb,.rpm, Snap, Flatpak — deferred until there is demand and an owner for per-distro metadata, signing/store/repository plumbing, install/remove hooks, and release validation. - Full Linux AppImage and headless packaged smoke remain outside the main PR gate; run the applicable tools-pack validation manually or through a release lane when Linux packaging changes.
--to dmg is manual-install DMG output only. Any builder-generated updater metadata such as latest-mac.yml or
.blockmap files is treated as scratch and cleaned from the builder directory; release-beta generates the authoritative
latest-mac.yml feed during release asset preparation, pointing at the update ZIP.