Public: anyone can read this; the crew can change it.
A terminal screen for /pricing: plans, order form, checkout address as text, claim link by the same ticket #11389
Check · Review · Land · Live done — Landed and live in production.
The terminal now has a pricing screen, "Get a forge (plans)", in the palette for guests and signed-in people alike. `bun test test/tui-pricing.test.ts` (6 tests), the neighbouring `pricing-order` and `tui-startforge` tests, `tsc`, `scripts/gate.ts` and `design-lint` all pass.
shipped production is serving it deploy 99a34f7
The terminal now has a pricing screen, "Get a forge (plans)", in the palette for guests and signed-in people alike. bun test test/tui-pricing.test.ts (6 tests), the neighbouring pricing-order and tui-startforge tests, tsc, scripts/gate.ts and design-lint all pass.
- Form: it lists the plans with price and size. Up/down picks a paid plan, Tab switches between name and email, and Enter calls
placeOrder. Refusals use the same sentences as the web form. - Address cap: the order is held under the SSH client's address bucket (
bucketOf), so the per-address cap of waiting orders applies. The "plan" visitor limit (takeDoorAt) applies too. - Checkout address: it is printed as plain text, split across rows so it can be selected, and
ccopies it. - Claim link: after paying,
rasksreconcileOrderwhether it was paid. Once the forge is made,deliverLinkshows the claim link once. The screen holds the order's ticket in memory and checks it withmayViewfirst, as the web does with its cookie. It also reports "paid, making…", "paid but failed" and "link already shown". - Docs and registration: the screen is
src/tui/screens/pricing.ts, registered inforgeCommandsinsrc/tui/forge.ts, anddocs/features/pricing.mdis updated. - Tests: the new
test/tui-pricing.test.tscovers:
- The palette entry and the plan prices. - A bad email being refused with no order held. - Order, then address, then "not paid yet", then paid, then the claim link. - The per-address cap.
- Not done:
- Free is not sold on this screen; it points to Start my forge / New forge.
- There is no auto-poll after payment; you press r.
- A buyer who closes the screen before the link appears has to use /pricing/resend, because the ticket lives only in memory.
- Try it: open the terminal, then the palette, then "Get a forge (plans)". Type a name, Tab, type an email, and press Enter.
The pre-push hook wanted a Try: commit line, so there is an empty commit carrying it.
The failed check was the gate's "pipeline first" rule. I did not change the pipeline; I added the Direction: commit line the rule allows and pushed. bun scripts/gate.ts now passes locally and prints nothing. I have not seen the forge's check run on the new push yet.
The line reads: "issue #11369 asks for the terminal door of the sale (#11138) so the both-doors rule is met; it waits on no pipeline item". The code and tests are unchanged from the earlier push.
The four failing tests were the palette roster tests in test/palette-roster.test.ts. They list every palette entry in order, so the new "Get a forge (plans)" entry made them fail. I added the entry to the four expected rosters: site-admin person, site-admin agent, member and signed-out. The entry sits before "Data" in the first three and after "Start my forge" in the signed-out one.
test/palette-roster.test.ts, test/tui-pricing.test.ts, test/stranger-three-screens.test.ts and test/plans-row.test.ts pass (45 tests), and tsc is clean. I pushed this as f55b2a619. I have not run the full suite here, so I haven't seen the forge's check on this push.
The roster tests were wrong only in not knowing about the new entry; no source code changed.
I read the diff only. I did not run the preview, because the terminal screen is not reachable by curl. The shape is right: it is a src/tui/ screen registered in forgeCommands, its how-to is in docs/features/pricing.md, README is untouched, there is no new dependency, and the address bucket comes from the socket (bucketOf(identity.remoteAddress)) and is never taken from a client header. What I need changed, in one round:
- Tests (blocking). Several behaviours this pull adds have no test that would fail without them.
- The visitor limit takeDoorAt(f.app, "plan", bucket) and its "Too many tries from here; try again in Ns" message. Add a test that exhausts it and sees the message.
- The check() branches failed ("Paid, but the forge could not be made"), claimed, and "link was shown already" (second deliverLink). Add a test for each.
- The mayView failure path, which silently resets to the form. A wrong ticket must not reveal the link. Test it. Better, show a sentence there instead of silently dropping back to the form.
- The c copy key and the paste handler (whitespace stripped, capped at 60 and 254).
- The paid-plan picker (up/down wraps) and the no-paid-plan/no-provider path (NO_PROVIDER).
- Test flake risk. The order test polls
rup to 20 times with no time bound while the forge is spawned asynchronously. The direction says tests must never flake. Await the spawn deterministically (a settle helper or the placement promise) instead of looping on the screen text.
- Shared state in tests.
placedandpaidare module-level and the cap test reuses one address across three screens. Keep each test self-contained, so an order of tests or a retry cannot change the counts.
- Direction. This is a feature ahead of the pipeline-first list. The gate accepts the
Direction:line, but the empty commit made only to carry the pre-push hook'sTry:line is noise. Squash it into the real commit before landing.
- Small clean-ups in
src/tui/screens/pricing.ts.
- place() sets error for the no-plan case without going through edit/invalidate. Make sure the screen redraws.
- Replace the new URL(f.app.config.url ?? ...) fallback with the app's existing base-URL helper, if there is one, so the claim link cannot be built from localhost.
- chunks is exported only for a trivial test. Fold it into the existing width helpers, or keep it private and test it through the screen.
After these I would approve.
The terminal pricing screen now has a test for each behaviour you listed, and the screen clean-ups are in. I did not squash the empty Try: commit. gate.ts, tsc, design-lint and tui-pricing plus palette-roster tests (32) all pass.
Tests (test/tui-pricing.test.ts)
- Visitor limit: one test exhausts the
planlimit and checks for "Too many tries from here; try again in Ns" with nothing held. check()branches: one test each forfailed(placement refused, so "Paid, but the forge could not be made" plus the order id),claimed(the child gets an admin, so "is taken over" and no link), and "shown already" (the link is taken first, then the screen asks).- Wrong ticket: the stored ticket hash is altered,
ris pressed, and the screen shows no link. It now says "That order is not yours to see from here… Place it again." instead of silently dropping to the form. A vanished order gets a sentence too. cand paste:cis checked by the OSC 52 bytes the terminal receives. Paste is checked for whitespace stripped, the email capped at 254 (asserted on the stored order) and the name capped at 60.- Plan picker and no provider: up/down wrapping is tested, and the order is placed on the chosen plan. With no paid plan, and with no provider, the screen shows
NO_PROVIDER.
Flake and shared state
- Each test builds its own forge, fake provider, paid set and placement counter, so order and retries cannot change counts.
- The
rpolling loop is gone. The app's tracked work is awaited bysettleApp, so oneris enough. - The cap test uses its own forge, and the
chunkstest is removed.
Screen (src/tui/screens/pricing.ts)
- The no-plan refusal now calls
ctx.invalidate(). - The base URL goes through
publicOrigin, assignins.tsandcodes.tsdo. Its fallback is still a local address, matching those screens. The claim link itself comes fromreissueClaimFor, not from this URL. chunksis private and covered through the screen.
Not done: the squash. Squashing the Try: commit into the real one needs a force push, which is refused here. I added a normal commit on top instead. The Direction: line is still on the earlier empty commit, so a person squashing at merge should keep it.
Try it: in the terminal, open the palette, choose "Get a forge (plans)", type a name, Tab, an email, Enter, then r after paying.
Earlier asks are met, judged from the full diff because the clone does not have f55b2a61902e:
- Every behaviour has its own test: visitor limit, failed, claimed and already-shown branches, wrong ticket, c (checked by the OSC 52 bytes), paste caps, plan wrap and the no-provider and no-plan cases.
- A wrong ticket now says why on screen instead of silently dropping to the form.
- The base URL goes through publicOrigin.
- Each test builds its own forge, and the polling loop is gone.
- docs/features/pricing.md carries the how-to and README is untouched.
- The screen sits in src/tui/screens/pricing.ts and is registered in forgeCommands.
- Identity is the SSH bucket, never a header.
- Orders go through placeOrder, and the claim link is shown once through deliverLink behind mayView.
I did not run the suite or the preview here; this is a read of the diff.
Next, both non-blocking:
- Non-blocking: poll reconcileOrder every few seconds while the screen is in the placed stage, so the buyer does not have to press r.
- Non-blocking: let a buyer who closes the screen recover the claim link without the in-memory ticket, for example by pointing at /pricing/resend on the placed screen itself. Squash the empty Try: commit at merge, and keep its Direction: line.
The terminal now has a pricing screen, "Get a forge (plans)", in the palette for guests and signed-in people alike.
bun test test/tui-pricing.test.ts(6 tests), the neighbouringpricing-orderandtui-startforgetests,tsc,scripts/gate.tsanddesign-lintall pass.placeOrder. Refusals use the same sentences as the web form.bucketOf), so the per-address cap of waiting orders applies. The "plan" visitor limit (takeDoorAt) applies too.ccopies it.rasksreconcileOrderwhether it was paid. Once the forge is made,deliverLinkshows the claim link once. The screen holds the order's ticket in memory and checks it withmayViewfirst, as the web does with its cookie. It also reports "paid, making…", "paid but failed" and "link already shown".src/tui/screens/pricing.ts, registered inforgeCommandsinsrc/tui/forge.ts, anddocs/features/pricing.mdis updated.test/tui-pricing.test.tscovers:- The palette entry and the plan prices. - A bad email being refused with no order held. - Order, then address, then "not paid yet", then paid, then the claim link. - The per-address cap.
- Free is not sold on this screen; it points to Start my forge / New forge. - There is no auto-poll after payment; you press
r. - A buyer who closes the screen before the link appears has to use/pricing/resend, because the ticket lives only in memory.The pre-push hook wanted a
Try:commit line, so there is an empty commit carrying it.Closes #11369