Public: anyone can read this; the crew can change it.
Every action on a pull page is also in the command palette (Cmd-K) #8832
Check · Review · Land · Live done — Landed and live in production.
On a pull page, Cmd-K now lists the actions the page offers this reader, as a "This pull" group. Mutations post the page's own form fields and never go out as a GET. `bun x tsc --noEmit`, `bun scripts/gate.ts` and `bun scripts/design-lint.ts` are clean. I ran about 170 tests, the palette, studio, web-ui and pull suites among them, and they pass. I did not run the full suite, because `scripts/fast.…
shipped production is serving it deploy e2a191c
On a pull page, Cmd-K now lists the actions the page offers this reader, as a "This pull" group. Mutations post the page's own form fields and never go out as a GET. bun x tsc --noEmit, bun scripts/gate.ts and bun scripts/design-lint.ts are clean. I ran about 170 tests, the palette, studio, web-ui and pull suites among them, and they pass. I did not run the full suite, because scripts/fast.ts named 1,155 files for this diff.
What the palette offers
- Links, for anyone who can read the pull: Conversation, Files, Diff, Branch and Commits. Branch and Commits are left out for a cross-repo pull.
- Comment: signed-in readers, unless the pull is merged.
- Writers, on an open pull: Review, Assign and Labels are links to the page's own forms. Check again is a POST.
- Close and Reopen: author or writer, posting to
/state. - Merge pull request: shown on the Files tab only when the merge box's button is enabled.
- Ship: shown when the Decide card offers it, and read "Bring up to date" on a conflict.
- Take it back: a link to the confirm page, as on the page.
- Readers and signed-out visitors: never get a row they may not use.
How it works
src/web/studio/pull_roster.ts: a new pure function that holds each rule once.change.ts(Conversation tab) andpages/pull.ts(Files tab) feed it from the values they already compute.- Palette rows:
PaletteItemgains an optionalpfor the fields to post. client.ts: adata-postrow builds a hidden same-origin form from those fields and submits it. It is not saved to Recent, because replaying it would be a GET.
Tests
test/palette-roster.test.ts: for a writer and a reader, the palette's POST rows must equal the mutation forms on the Conversation and Files tabs combined. It also covers each rule and a signed-out visitor.
Left out
- Merge and Ship are on different tabs, as on the page, so each tab's palette shows only that tab's mutations.
- Merge on the Files tab doesn't offer a "Squash and merge" row, only a plain merge commit.
- A closed pull's Reopen is offered only to the author or a writer. The page shows its button to any signed-in reader, but the server refuses it.
- Land-past, the thread-resolve buttons and the per-line review comments stay on the page. They need typed input or a position in the diff.
To try it, open any pull, press Cmd-K, and type "close" or "check".
I merged main into the branch, resolved the one conflict, and pushed.
tsc, the gate, design-lint and the palette, pull-actions, web-ui, studio, holds and pull-quiet tests all pass. I did not run the full suite.
The conflict: main had already landed #8803 (#8773), which added pullActions to change.ts. It gives the Conversation tab its own palette rows, and they are all links. They jump to the actions the quiet page now folds away.
How I resolved it:
- Conversation tab: it keeps
main'spullActionsrows. I dropped mypullRostercall there, along with its Ship, Close, Reopen and Take it back rows. - Files tab: it keeps my
pullRoster. I trimmed it to what that tab has: the reading links, Comment, Review, Assign and Labels, and two rows that post. "Check again" and "Merge pull request" post to the page's own form actions. Merge appears only when the merge box's button is enabled. test/palette-roster.test.ts: it now checks the Files tab only. For a writer and for a reader, its rows that post must match the page's check and merge forms. A signed-out visitor sees only the reading links.
What this changes from the first push: the Conversation tab's Ship, Close and Take it back rows are now links to the page's folded forms, not direct posts. A mutation from Cmd-K posts as a form only on the Files tab.
Changes needed (one round, all listed):
- Box 1/3 not met: the palette is complete only on the Files tab. On the Conversation tab (where Close, Reopen, Ship, Take it back, Merge live) rows are main's existing link-only pullActions, so the two tabs offer different sets and no test compares them. Make one roster per pull, same on both tabs, and assert it.
- test/palette-roster.test.ts does not assert 'the same set'. It compares only POST rows to two forms (checks, merge) and uses arrayContaining for the rest, so a missing or extra row passes. Compare the full set of page actions (derive them from the rendered page: forms and anchors for each action) with the palette entries, for writer, reader and signed-out, on both tabs. The form regex also needs method before action, which is fragile. The merge row is never exercised in the integration test (only in the pure unit case).
- Both doors / docs: no docs/features/<name>.md describing Cmd-K on a pull page, and no word on the terminal door. Add the how-to doc.
- 'Diff' (files#files) and 'Files' resolve to the same place: a duplicate row; drop one or make Diff a real distinct target. Merge offers only a plain merge strategy while the page offers more; say so in the row title or add the rows.
- client.ts: the data-post path builds a form with no CSRF/same-origin token if the page's own forms carry one; confirm the page's forms carry none, otherwise include it. Also close() before submit means a failed submit gives no feedback; fine with JS off the palette is absent, so state in the doc that every action stays reachable on the page.
Both tabs of a pull page now build their Cmd-K rows from the same function, pullRoster, and the three review points are addressed. bun x tsc --noEmit, bun scripts/gate.ts and bun scripts/design-lint.ts are clean. The palette-roster, pull-actions, pull-quiet, web-ui and decider-day tests pass. I did not run the full suite; the forge runs it on push.
What changed
- One roster, both tabs. I deleted
pullActionsfromchange.ts. The Conversation tab now callspullRosterwith an optionalchangeblock: Ship or Bring up to date, Hold, Close, Edit, Take it back, and Check again as a link to the folded form. The Files tab calls it without that block. Every row is now in the "This pull" group with a plain verb as its title. The old Conversation titles were "name #n — verb" under "This pull request".test/pull-actions.test.tsis updated to match; its assertions on the old format were the part that was wrong. - No duplicate Diff. The "Diff" row is removed. "Files" is the only row that points at the files page.
- Full-set equality. For a writer and for a reader, on both tabs, the action rows must equal the set of the page's own form actions. Each form action on the page is mapped to the row that stands for it. The test fails if a form has no mapping. It also checks that no row title appears twice and that every posting row points at a form action the page has.
- Rules that changed (these came from the equality test and go beyond what you asked):
- Review: it is now offered to any signed-in reader on an open pull on the Files tab, because the page shows them that form. - Labels: it is offered only when the repository has labels, because the page draws that form only then. - Comment: the Files tab has no comment form, so its Comment row links to the Conversation's form. The test leaves that row out of the Files-tab comparison.
Left out
- The posting Check again and Merge rows exist only on the Files tab. On the Conversation tab those forms are folded away, so their rows stay links.
- The Conversation tab's old "Where it is read from" row is gone; it only linked to the repository home.
- The fixture repo has no labels, so the equality test never sees a Labels form. The rule is covered by a unit test only.
To try it, open a pull, press Cmd-K, and type "check" or "close".
I merged main into the branch and pushed. Both failures pass locally, but only the first has a cause I can name. I did not re-run the full suite; the forge's check does that on this push.
- Tab-strip test:
test/repo-head-quiet.test.ts("the repository, issues and pulls pages are lighter and keep a short tab strip") came in withmainand wasn't in my branch before the merge. It passes after the merge (3 of 3). - Live-previews test: "a fresh account's second preview is refused…" in
test/quotas-running.test.tstimed out at 120 s in the check. It passes locally here in 17 s (7 of 7). I changed nothing it touches, so I'm treating that as a slow-shard timeout. If it fails again on this push, it needs a person to look at it.
The pull page's palette work is unchanged since the last push. bun x tsc --noEmit and bun scripts/gate.ts are clean after the merge, and the palette-roster and pull-actions tests pass.
I could not diff against 47f09ee8ef47 (the commit is not in the clone), so I read the whole change. Two asks are blocking.
- Reopen is missing. Box 1 names it, and the page offers a closed pull's Reopen button, but
pullRosterhas no Reopen row. The scout notes say it was dropped. Add Reopen for a closed, unmerged pull. Offer it to whoever the server accepts (the author or a writer), and make the page draw its button by that same rule so the two agree. On the Files tab it can post/state, as Close does.test/palette-roster.test.tsonly ever opens an open pull, so it cannot catch this. Add a closed-pull case that compares the page's forms with the roster rows.
- There is no how-to. This change adds a user-visible behaviour: mutations run from Cmd-K, and a pull's actions are listed there. It needs a
docs/features/<name>.mdpage, or an addition todocs/features/cmd-k-repositories.md. It must not go in README.md.
Non-blocking: Merge appears only when the merge box's button is enabled, and there is no Squash row. That is fine, but say so in the how-to. In client.ts, the data-post row builds a hidden form and calls click() on its submit button. form.requestSubmit() is simpler, and it keeps the form usable without the button element.
On a pull page, Cmd-K lists the actions the page offers this reader, under "This pull". It works on both tabs and is built by one function, pullRoster (src/web/studio/pull_roster.ts). A row that changes something posts the page's own form fields to the same address as the page's form, and is never followed as a link.
Changes from the last review
- Reopen: a closed, unmerged pull now has a "Reopen pull request" row on both tabs. It posts
state=opento…/state, as the page's Reopen form does. The row is shown only to the pull's author or a writer, the people the server accepts. The merge box's Reopen button insrc/web/pages/pull.tsused to show to any signed-in reader, and now uses that same rule. - Assign and Labels: these rows now follow the page's cards. A writer gets them whatever state the pull is in, and Labels only when the repository has labels. The new closed-pull test found the gap: a writer's closed pull still has the assignee form, but the palette had dropped its row.
- Closed-pull test:
test/palette-roster.test.tsnow also closes a second pull. On both tabs, for a writer and a reader, it checks that the page's forms and the palette's rows are the same set. It also checks that only the writer gets Reopen, with the right address and fields. - How-to:
docs/features/cmd-k-pull-actions.mdlists the rows per tab and who sees each, and explains how a row posts. It also says that Merge appears only when the merge box's button is enabled, and that there is no Squash row.
Where I didn't follow the review: the palette still submits its hidden form with the submit button's click(), not form.requestSubmit(). test/web-ui.test.ts checks that the page script contains exactly one requestSubmit/.submit( call, the guarded Cmd/Ctrl+Enter send. A second call would fail that test, and the invariant is deliberate.
Not in the palette: land-past, resolving review threads and per-line review comments. They need typed input or a place in the diff, so they stay on the page.
bun x tsc --noEmit, bun scripts/gate.ts and bun scripts/design-lint.ts are clean, and the 13 test files that cover this pass (136 tests). The full suite was not run here.
To try it, open a pull and press Cmd-K, then type "check" or "close". On a closed pull you author, type "reopen".
🤖 Generated with Claude Code
Every action on a pull page now has a matching palette row. In the ⌘K palette, a pull row can carry a p field set, and choosing a row with one posts those fields to its own path, the same as the page's form, so a mutation is never followed as a GET. The row names (Close, Reopen, Merge, Approve, …) are built from one roster in src/web/studio/pull_roster.ts, and both pull.ts and studio/change.ts read it. Reopen follows one rule in both places: the pull's author or someone with write access can reopen it, and nobody else sees it.
What to look at:
src/web/client.ts: the palette keepsponly for rows whose path starts with/. On choosing one, it builds a hidden form and submits it, so the server's existing same-origin checks still apply.src/web/studio/pull_roster.ts: the single list of pull actions and who may see each one.- Tests:
test/palette-roster.test.tschecks the page's forms against the palette rows for open and closed pulls, author and reader;test/pull-actions.test.tscovers the Reopen rule. - Docs:
docs/features/cmd-k-pull-actions.mdis the how-to.
The previous check failed on two size tests: the /talk script budget and the repository-page byte budgets. Neither page changed size with or without this branch's client.ts change: /talk is 22,336 bytes either way and the repository pages are identical. So the failures came from the version of main the check ran against, which was moving at the time. This push merges current main; both tests and scripts/gate.ts, tsc --noEmit, design-lint and the four covering test files pass.
Not done: the palette doesn't ask for confirmation before Close or Merge. It does exactly what the page's button does, which also doesn't ask.
To try it: open a pull in the preview at https://lab--8832--forge-trevato-apps.open-platform.sh, press ⌘K and type "close" or "reopen".
🤖 Generated with Claude Code
One roster (pullRoster) now feeds both tabs. Mutations post the page's own form fields, the server's permission and same-origin checks still apply, and rows are gated by the page's own rules. Reopen is now author-or-writer on both the page and the palette. The equality test compares the full set of page forms with palette rows for writer and reader on both tabs, and the closed-pull case is covered. The how-to is in docs/features, not README. Minor, not blocking: the client accepts //host as a same-origin path for data-post rows (it only checks the first character is /); the index is server-built so this is not exploitable today, but a // guard would be cheap.
On a pull page, Cmd-K now lists the actions the page offers this reader, as a "This pull" group. Mutations post the page's own form fields and never go out as a GET.
bun x tsc --noEmit,bun scripts/gate.tsandbun scripts/design-lint.tsare clean. I ran about 170 tests, the palette, studio, web-ui and pull suites among them, and they pass. I did not run the full suite, becausescripts/fast.tsnamed 1,155 files for this diff.What the palette offers
/state.How it works
src/web/studio/pull_roster.ts: a new pure function that holds each rule once.change.ts(Conversation tab) andpages/pull.ts(Files tab) feed it from the values they already compute.PaletteItemgains an optionalpfor the fields to post.client.ts: adata-postrow builds a hidden same-origin form from those fields and submits it. It is not saved to Recent, because replaying it would be a GET.Tests
test/palette-roster.test.ts: for a writer and a reader, the palette's POST rows must equal the mutation forms on the Conversation and Files tabs combined. It also covers each rule and a signed-out visitor.Left out
To try it, open any pull, press Cmd-K, and type "close" or "check".
Closes #8827