Cloud page: each machine shows its cost ($/h · Running · About $X so far), as the sidebar does
From HQ #4755 (cloud comparison, 2026-10-30) and #5306's Usage row:
The in-app Cloud page (webviews/src/pages/cloud) shows no cost; hourly appears nowhere in it. Its CloudMachine in ops.ts doesn't carry hourly_rate_usd or running_since.
Small next slice: the cost line on the Cloud page's machine list and detail, from the same server fields the sidebar reads. This needs the backend projection to carry them, so it's a client change plus a backend one.
Leo's ask behind #5306: "How about more cmux-platform audits, improvements, features, fixes, optimizations, polish, plus the ui that gets people (or agents) using it".
What changes
The Cloud page reads machines from the Cloud app server (first-party-apps/cloud/server), which relays CloudDO's cloud.machine.list / cloud.machine.upsert from backend/apps/api. That record had no cost fields, and the app server's Machine drops fields it doesn't know. The sidebar's numbers come from classic /api/vm (web/services/vms/machineCost.ts), which the page doesn't call. So this PR changes three layers:
- Backend (
backend/apps/api, CloudDO):publicMachinenow addshourly_rate_usdandrunning_since(domains/cloud-cost.ts). The rate is Freestyle's vCPU and memory list price for the machine's size. Both transports run on Freestyle (the direct provider and the VM API's upstream), so 4 vCPU / 8 GB comes to $0.2645/h. Nothing here reads classic/api/vm; its rate table is cited only so the two clouds show the same number.running_sinceis a new privaterunning_at(set at bind and when a start the provider confirmed lands; older rows fall back tolast_power_at) while the machine isrunning, and null otherwise. Both fields are optional in the protocol'sCloudMachineschema, and the catalog, TS client, Rust wire types andcmux-app.d.tsare regenerated from it. The contract doc lists them. Per cmux-platformdocs/port-cloud-worker.mdrule 1, the fix goes into cmux first and moves over with the port. - Cloud app server (Rust):
Machinepasses both fields through. The rate is kept as a JSON number, so the type staysEq. - Cloud page:
cost.tsformats the line in the sidebar's words and uses the sidebar's translations (SidebarMachineCost+Text.swift):$0.26/h · Running 2h 14m · About $0.59 so far,Under $0.01 so far, or$0.13/h when runningwhile paused.- In the list, a running machine's subtitle shows this line in place of "Running". It already says "Running 2h 14m".
- Other states show
Paused · $0.13/h when running. - The detail's Overview gets a Cost row.
- Nothing shows when the server sends no rate, for example a classic machine.
Timer: useCostClock refreshes "now" once a minute, and only while a shown machine is running with a start time. The effect's cleanup clears it when no shown machine is running anymore, or when the list or detail unmounts. It does no per-row work beyond formatting the rows already on screen.
Size
About 1,550 lines, of which about 1,240 are generated or localization:
Localizable.xcstrings: 792 lines (6 keys × 21 languages).cloud-operations.json: 441 lines (the two fields inlined in each machine-bearing op).
The hand-written part is about 300 lines with tests. I didn't split it: the backend fields mean nothing without a reader, and the page can't show anything without the fields.
Risk
risk:normal.
- The backend change is additive: two optional fields on a public read. No write path or private field changes.
last_power_atstays out of the record, and only its value while running is exposed. - Old clients ignore the fields, and the page shows nothing without them.
- The rate is a display estimate, not a bill (the same caveat as the sidebar).
Tests
webviews/src/pages/cloud/cost.test.ts: wording, two-unit durations, under a minute, paused, and no rate.strings.test.ts: every language, matching placeholders.backend/apps/api/test/cloud-machine-cost.test.ts: rates match classic ($0.2645, $0.1322); running carriesrunning_since; paused gets null;last_power_atstays private.models.rsmachine_cost_fields_relay_to_the_page: round trip, null, and older records.
Observed / Not run
- Ran locally:
bun test src/pages/cloud: 47 pass.tsc -p .forbackend/apps/apiandbackend/packages/protocol: clean.bun run catalog:checkandgen-cmux-global.ts: up to date.rustfmt --checkonmodels.rs: clean.prepush-gate.py: no new violations.
- Web lint: the only reds were
check:design-tokens,check:layers,vp fmt --checkandvp lint. All four fail onwebviews/src/pages/settingsat base5f1a9ce9cfb, and this PR doesn't touch that folder. - CI at
c59bef6155b5, all green:- Backend vitest: 227 files, including
cloud-routes,cloud-start-pauseandcloud-machine-cost. first-party-apps/cloud/servercargo test, includingmachine_cost_fields_relay_to_the_page.cloud/cost.test.tsin the webview shards.
- Backend vitest: 227 files, including
- The reds at this head are all outside the files this PR changes:
react-apps-check: the optchat inspector page is stale, plus agent-pane and new-tab tests.- The clippy panic ratchet: hits in
cmux-automationsandcmux-tui. tooling-tests: the test registry's linux-guard lane.cmux-next checks: Tier routing.- Also
release-notary(502s), the review-linelinkjob, and the Release compile (no compile result, since this PR has no Swift change).
Review
- The
router-review(gpt-6-astra) BLOCK ata4a09b19was fixed ind1881ade: the vectors now carry the cost fields for the exact-shape route test. - Its two P2s at
d1881adewere fixed inc59bef61:running_sincenow comes from a privaterunning_at, set when a start lands or at bind, not when the start request was made.- The resize vector's rate now matches its new size.
- SHIP at
c59bef6155b551f5e05d504f9504d15dd6c4e8a8, with no follow-ups.
Screenshots
These come from this head's gallery advisory, pages.cloud machines state, at 2x (synthetic mock data), cropped to the machine list and the Overview. I viewed the after images.
- Visual verdict: one line per row, with no new control on any row.
- The running row's subtitle swaps "Running" for the cost line, which already says "Running 2h 14m".
- The paused row adds "· $0.13/h when running".
- The Overview adds one Cost row.
- The row and the Cost row both fit at the narrow width (560).
| Before | After | |
|---|---|---|
| Light | ![]() | ![]() |
| Dark | ![]() | ![]() |
Changelog
- Added: The Cloud page shows each machine's cost, as the sidebar does: its hourly rate, how long it has run and about how much so far.
Agent session: agent session
🤖 Generated with Claude Code



