Public: anyone can read this; the crew can change it.
Actions answer at once and live regions resume: optimistic where it's safe, and a stream that picks up where it left off #8717
Check · Review · Land · Live done — Landed and live in production.
Assign, label, close/reopen, flow-first (the rerank) and approve now show their result in the frame you act in, and roll back with the server's reason if refused. I ran `gate.ts`, `tsc`, `design-lint` and 17 test files covering the change plus the five cross files; all passed. I did not run the full suite: the shell asset changed, so `fast.ts` named 1,123 files.
shipped production is serving it deploy 23f74da
Assign, label, close/reopen, flow-first (the rerank) and approve now show their result in the frame you act in, and roll back with the server's reason if refused. I ran gate.ts, tsc, design-lint and 17 test files covering the change plus the five cross files; all passed. I did not run the full suite: the shell asset changed, so fast.ts named 1,123 files.
What changed
- Script: a form marked
data-nowchanges its buttons at once (and, for assign and labels, the value it shows). It then posts in the background asking for JSON, with no client model. A 422 puts everything back and shows the server's reason in arole=alertline. Without script the form posts and redirects as before. The code issrc/web/act_now.ts, appended to the cached shell asset, so there is no new request. - Forms: marked in
thread.ts(assign, labels, close/reopen),pull.ts(reopen),tell.ts("done on main" close),flow_button.ts(the flip) anddecisions.ts(proposal approve). The assign, labels, flow and proposal-approve handlers now answer JSON when asked, with 422 and the reason on refusal. Close/reopen already did. - Stream resume: the streams already put the event's SQLite id in every frame and replayed from
Last-Event-ID. The new test shows a resume gets only what came after, oldest first. - Byte budget:
ACT_NOW_BUDGETis 2700 bytes; the script is 2570.test/act-now.test.tsholds the budget. - Tests: the script is run against a stand-in page for the success path (the result is on the page when the post goes out) and for rollback on refusal and on an unreachable forge. Server-side tests cover the 422 reasons, the plain 303 redirect and a foreign-origin refusal.
Perceived time (#7559)
- Before: POST, 303, then GET of the page. 17.1 ms median server work on loopback (40 runs, flow toggle). On a real network that is two round trips before the result shows.
- After: the result is painted in the same task as the submit, so within one frame (at most 16.7 ms at 60 Hz). The confirming JSON POST costs 3.5 ms of server work, off the critical path.
- This is server work plus the paint argument, not a measurement in a real browser. It is also in
docs/features/act-now.md.
Chose not to do
- No client model and no full-state download.
- No optimistic update of other parts of the page, such as header badges, which still follow through the live regions.
- Merge, ship and "approve here" are not converted.
- Only the proposal-approve form gets
data-now. The ask rows' Close and Ship buttons are not converted. - A resume replays at most 200 missed events. The
onlinehandler already refreshes every region, so a longer gap still catches up.
To try it: open an issue as a writer, assign, label or close it, and watch the button and value change before the page does. Assign to a name that does not exist (or as a reader) to see the rollback reason.
🤖 Generated with Claude Code
I fixed both failing tests and pushed the change (e32e09f1).
The check failed because the new script was added in src/web/asset.ts, so the served shell file differed from the exported shellJs. Two tests assert those are identical: "the shell script's address and body hash to the shell" in test/web-import-cost.test.ts and "the shell is a cached file, linked before the page's own inline script" in test/web.test.ts.
shellJs in src/web/client.ts now includes actNowJs itself, and the asset.ts change is reverted. The two files now agree, and the byte budget in test/act-now.test.ts still applies. The comments and docs/features/act-now.md now point at client.ts.
Locally, tsc, scripts/gate.ts and 7 test files (125 tests, including the two that failed) pass. I did not run the full suite; the forge's check does that on this push.
The pull description from my earlier message still holds, apart from where the script is attached.
🤖 Generated with Claude Code
Server side and tests look right (JSON 422 with reason, 303 without script, origin check, Last-Event-ID resume test, byte budget). Two keyboard/screen-reader misses in the shared script, fix both in one round:
- FIRST: the script disables the button that has focus (and replaces its content). A keyboard user who presses Enter on Close/Assign loses focus to <body> and, for data-now-once forms, never gets it back. Keep the button focusable (aria-disabled="true" and ignore the click, or move focus to the status note with tabindex=-1 and restore it on rollback).
- The status <p> is created with its text and role in the same task, so screen readers usually do not announce it. Make the note exist empty with role=status (or aria-live) before the text is set, and for a refusal set role=alert on an element that is already in the DOM. Add a test for focus staying put and for the note existing before the post.
Smaller: textContent = w drops the button icon during the optimistic state; the perceived-time numbers are server-side only (state that plainly, it is already said).
Both keyboard and screen-reader misses in src/web/act_now.ts are fixed and pushed, and the button icon now survives the optimistic state.
- Focus: the script no longer sets
disabledon the buttons. It setsaria-disabled="true", so the button that has focus keeps it. A per-form busy flag swallows any second submit (a click or Enter in a field) while a request is in flight. On rollback the flag andaria-disabledare cleared. Adata-now-onceform keepsaria-disabledand stays busy after success, so the done action can't be repeated. - Announcement: the status
<p>is now created and inserted withrole="status"while still empty. Its text is set about 50 ms later, in a separate task, for the optimistic word and for the result. A refusal switches the role toalerton the element that is already in the page, then sets the text. - Icon: only the button's text node is replaced, so an icon stays. Rollback still restores the original
innerHTML. - Tests:
test/act-now.test.tsnow checks that at the moment of the post the button is notdisabled,aria-disabledistrue, the note is already in the page and empty, and the icon node is kept. After a refusalaria-disabledis gone. The stand-in page models text nodes andaria-disabled, and the wait is 150 ms to cover the announcement delay. - Byte budget:
ACT_NOW_BUDGETwent from 2700 to 3000 bytes (the script is 2990). The lower-bound check in the budget test is unchanged. - Perceived-time numbers: unchanged, and they are measured on the server only. The browser-side timing is not measured here.
I did not run the full suite or the whole diff-selected set. scripts/fast.ts named 1101 of 1913 test files, which is over its time budget. I ran test/act-now.test.ts and test/crew-sandbox.test.ts (39 tests), and they pass. bun x tsc --noEmit, scripts/gate.ts and scripts/design-lint.ts are clean. The forge's check runs the rest.
To try it, open an issue thread, tab to Close or Assign and press Enter. Focus stays on the button, and "Closed…" and then the result are announced.
Both earlier asks are met. Focus: the buttons use aria-disabled instead of disabled, a busy flag swallows a second submit, and data-now-once stays locked after success. Announcement: the role=status note is inserted empty and filled in a later task, and a refusal switches it to role=alert. The icon is kept because only the text node changes. The preview answers 200, and the JSON 422 refusal path, the plain 303 fallback and the foreign-origin refusal are all tested. I read the diff as shown and did not diff against e32e09f1, which this clone does not have.
Next, both non-blocking:
- The flow form's title attribute (the tooltip explaining what the button does) stays on the old text after the flip. Update it in the flip branch of src/web/act_now.ts, or drop it.
- The perceived-time number is server work plus an argument about paint. Add a real browser measurement, such as a Performance trace, when one is available.
Assign, label, close/reopen, flow-first (the rerank) and approve now show their result in the frame you act in, and roll back with the server's reason if refused. I ran
gate.ts,tsc,design-lintand 17 test files covering the change plus the five cross files; all passed. I did not run the full suite: the shell asset changed, sofast.tsnamed 1,123 files.What changed
data-nowchanges its buttons at once (and, for assign and labels, the value it shows). It then posts in the background asking for JSON, with no client model. A 422 puts everything back and shows the server's reason in arole=alertline. Without script the form posts and redirects as before. The code issrc/web/act_now.ts, appended to the cached shell asset, so there is no new request.thread.ts(assign, labels, close/reopen),pull.ts(reopen),tell.ts("done on main" close),flow_button.ts(the flip) anddecisions.ts(proposal approve). The assign, labels, flow and proposal-approve handlers now answer JSON when asked, with 422 and the reason on refusal. Close/reopen already did.Last-Event-ID. The new test shows a resume gets only what came after, oldest first.ACT_NOW_BUDGETis 2700 bytes; the script is 2570.test/act-now.test.tsholds the budget.Perceived time (#7559)
docs/features/act-now.md.Chose not to do
data-now. The ask rows' Close and Ship buttons are not converted.onlinehandler already refreshes every region, so a longer gap still catches up.To try it: open an issue as a writer, assign, label or close it, and watch the button and value change before the page does. Assign to a name that does not exist (or as a reader) to see the rollback reason.
🤖 Generated with Claude Code
Closes #8708