Skip to content

Give a site's tickets their own card between the ticket and the patch panels - #242

Merged
juanmaguitar merged 3 commits into
juanmaguitar/changes-note-sees-ticket-workfrom
juanmaguitar/tickets-own-card
Aug 10, 2026
Merged

Give a site's tickets their own card between the ticket and the patch panels#242
juanmaguitar merged 3 commits into
juanmaguitar/changes-note-sees-ticket-workfrom
juanmaguitar/tickets-own-card

Conversation

@juanmaguitar

Copy link
Copy Markdown
Collaborator

Why

The Trac ticket card carries four different things once a ticket is linked: which ticket you are
on, the other tickets you have work on in this site, the pull requests that cite the ticket, and its
Trac attachments. Three of those describe the ticket in front of you. The fourth is a list of
everywhere else you could be instead — and switching between tickets is one of the main things a
contributor does on a site, which is the whole point of tickets being cheap. It read as a
sub-section of something else, set in a smaller heading than the card it sat inside.

Closes #240.

What changes

The list of a site's tickets gets a card of its own, at the same level as the Trac ticket card and
the "Apply a patch or PR" card, positioned between them. The reading order now matches what someone
is actually deciding: which ticket am I on, which of my tickets do I want, bring in work from
elsewhere.

This is placement and heading level, not a rewrite of the list. The row markup, the switch links,
"Delete this ticket's work" and its confirm, the ticketActionsBlocked disabling and the ordering
are byte-identical — renderBranchRows moved, it did not change. What is new is one exported
helper, ticketListCard in src/renderer/ticket-branch-list.cjs, holding the two decisions the
card makes: which heading the state calls for, and that no rows means no card rather than an empty
one.

It moves in both states, deliberately. Linked ("Other tickets on this site") and unlinked
("Your tickets on this site") both leave the ticket card. Making it conditional would mean the same
list jumps between two places depending on where you are, and unlinking would move it. The
consequence is accepted: on a site with no ticket linked, the Trac ticket card is left holding
little more than the field to type one into — which is honest, because on that site typing a ticket
number is the only thing there is to do there.

What is deliberately not in this PR: the switch feedback (ticketError, the switch progress
line, the carried/saved notices, and the "what should happen to your loose edits" panel from #234)
still renders in the Trac ticket card, not next to the rows that can now trigger it. See Risks.

How to test this

Platforms: any. Nothing here touches paths, spawning, line endings or signing.

Starting state: a site that is initialized and past the wizard, with work on at least two
tickets. Two runs, one per state.

With a ticket linked

  1. Open a site that has a ticket linked and work on at least one other ticket.
    → Three cards in this order: Trac ticket, Other tickets on this site, Apply a patch or
    PR
    . The tickets card is a card in its own right — same white panel, same border, same
    heading size as the two around it — not a section inside the ticket card.
    → The Trac ticket card now holds only the ticket pill with Open in Trac / Unlink, Linked
    pull requests, and Trac attachments.
  2. Click switch on a row.
    → The switch runs exactly as before. Nothing about the row behaviour changed.
  3. Start a trunk update (or an install), and while it runs look at the tickets card.
    → Every switch link and every Delete this ticket's work button is disabled, and comes
    back when the operation ends.
  4. Click Delete this ticket's work on a row and cancel the confirm.
    → Same confirm wording as before, nothing deleted.

With no ticket linked

  1. Open a site with no ticket linked but work on at least one ticket.
    Trac ticket (heading, one sentence, the input field and Link ticket, the "you will be
    asked what should happen to those edits" line, and the Browse-good-first-bugs link), then
    Your tickets on this site as its own card, then Apply a patch or PR.
  2. Click Continue working on #NNNNN.
    → Switches as before. If trunk has uncommitted edits you get the Uncommitted work on trunk is taken into a new ticket without asking, but refused for a ticket that already exists #234 question panel — note it
    renders at the bottom of the Trac ticket card, immediately above the tickets card, not
    inside the card you clicked in.

The empty case

  1. Open a site whose only ticket is the one currently linked (or a site with no ticket branches at
    all).
    No tickets card at all — not a card with a heading and nothing under it. The Trac ticket
    card sits directly above the patch card.

What must not have happened:

  • The list must not appear twice — once in the ticket card and once in its own. If you can see
    the same ticket offered in two places, that is the bug this change could most easily have
    introduced.
  • The list must not vanish in either state. Unlinking a ticket must keep it on screen, in the
    same card, with only the heading changing from "Other tickets" to "Your tickets".
  • An empty card must not be left behind after you delete the last other ticket's work — the
    card should disappear along with the last row, not linger as a heading.
  • The switch and delete controls must not become clickable during a long operation. That guard
    moved with them and is easy to lose silently: a switch during an install is a checkout of a
    working directory something else is writing to.

What could not be tested by hand: the #234 question panel appearing above the rows when a
switch is blocked from the new card. I could drive the app and confirm both card layouts, but the
synthetic clicks I used would not fire that link's handler, so the proximity claim in Risks below
is argued from the rendered geometry (measured on the captured screenshots) rather than observed.
Worth a click by a human reviewer.

Risks and limitations

Self-review: 2 [fix here] · 2 [follow-up] — both [fix here] addressed, see the collapsed block.

The one real consequence is that the switch feedback did not move with the controls. ticketError,
the switch progress line, the carried/saved notices and the #234 "what should happen to your loose
edits" panel all still render at the bottom of the Trac ticket card, while the switch and delete
links that set them now live in the card below. Nothing is swallowed — the ticket card is the
immediately preceding sibling in a gap: 24 column, so the panel lands directly above the row you
clicked, about 30px away in the unlinked state. In the linked state proximity actually improves:
the panel used to sit at the bottom of a card that also held Linked pull requests and Trac
attachments, a long scroll from rows near the top; now it is adjacent. Moving the feedback into the
tickets card was rejected because the same panel is also raised by the Link ticket field in the
ticket card above, so it would have to render in two places or be threaded through a shared slot —
a behaviour change, and this issue is explicitly placement only.

Nothing pins where that feedback renders relative to the rows, so if it is later moved next to
them, no test will notice. That is the assertion to add if it moves.

Related

Closes #240. Follow-up to #185, where this list came from — a layout change on top of it, not a
correction to it.


Design decisions and alternatives considered

Moving it only in the linked state. Rejected in the issue and not revisited. With a ticket
linked the list is a secondary way to move; with none linked it is the primary way to start, sitting
directly above the field for typing a ticket number. Tempting to leave the unlinked one where it is
for that reason — but then the same list jumps between two cards depending on state, and unlinking
makes it move. A list that stays put is worth more than a list that is optimally placed in each
state separately.

Where the heading choice lives. In ticket-branch-list.cjs, not inline in the JSX. Two
literals in a component of this size drift; the module already owns which rows exist and in what
order, so which heading they sit under belongs with them. The test asserts index.jsx does not
contain either heading string, which is what keeps the two copies from reappearing.

Why ticketListCard returns null rather than the JSX deciding. The "no rows, no card" rule is
the kind of thing that gets lost in a conditional chain. It is a decision, it has a reason (an
empty card is worse than no card — and unlike the card it left, this one has no input field to
justify the space), and decisions with reasons get a test. The JSX guard is then just
skipInit && ticketsCard.

Testing layout with a source scan. There is no DOM harness in this repo, and the whole point of
#240 is where the list renders — untestable through the module's return value alone. So the third
test reads index.jsx as text and asserts the ordering of the three card headings and that
renderBranchRows( is called exactly once. It is not a substitute for looking at the app, and it
is written to fail loudly with a specific message rather than subtly.

Review outcome (required — see AGENTS.md)

2 [fix here] · 2 [follow-up] — both [fix here] resolved.

Run per .github/instructions/code-review.instructions.md, with the judgement pass dispatched to a
subagent that had the diff and the instructions file and nothing from the session that wrote the
code.

Fixed:

  1. tests — the layout test asserted the inline style literal fontSize: 16 to pin the heading
    level. fontSize: 16 is repeated verbatim on all three card headings, so extracting them into a
    shared constant — an obvious, harmless cleanup — would have failed the test with the layout
    unchanged. Assertion dropped; the ordering assertion above it already covers the finding it was
    aimed at.
  2. tests — the "renders once" assertion counted the bare string renderBranchRows, which
    includes occurrences in comments, so a documentation edit naming the helper would have turned the
    suite red. Now counts renderBranchRows( calls and expects exactly one.

Deferred, with reasons:

  1. architecture — the switch feedback stayed in the Trac ticket card while its controls moved.
    Written up in Risks and limitations rather than fixed: the fix is a behaviour change (the
    same panel is raised from two places), and A site's tickets deserve their own card, not a sub-heading inside the ticket you are on #240 is explicitly placement only. The reviewer asked
    for a by-hand confirmation that the panel is still visible when a switch is blocked; I could not
    fire that handler under synthetic clicks, so it is called out as untested above.
  2. tests — nothing pins where that feedback renders relative to the rows. Deferred with 3: the
    assertion to add is the one that would fail when the feedback moves, and it should be written
    then, not now against the state it is not yet in.

Checked and clean: no double or zero render (one renderBranchRows definition, one call site,
Boolean(tracTicket) reproducing the old linked argument in both states); ticketActionsBlocked
still closed over from the same component; the empty-list guard; skipInit gating matching the two
neighbouring cards exactly. Security, performance and cross-platform are out of scope — no IPC, no
spawn, no path handling, no dependency, no electron-store shape change.

Implementation notes

Three files.

  • src/renderer/ticket-branch-list.cjs — adds ticketListCard({ rowCount, linked }), returning
    { heading } or null. Pure and dependency-free like the rest of the module, so node --test
    requires it directly.
  • src/renderer/index.jsx — deletes the two mutually exclusive sections (the linked one at the top
    of the ticket card, the unlinked one above the Link ticket field) and adds one card between the
    ticket card and the patch card, gated on skipInit && ticketsCard. renderBranchRows itself is
    untouched.
  • test/ticket-branch-list.test.cjs — three tests, all of which fail against the pre-change source.
    Proved by checking out the branch point into a detached worktree, copying the new test file in,
    and running it there: 3 failed, 15 passed. On the branch: 18 passed.

npm test 754/754 on Node 24.18.0 (.nvmrc) and 754/754 on Electron's bundled Node via
npm run test:electron, both re-run after the rebase onto #241. npm run lint clean.

Screenshots or recording

With a ticket linked — "Other tickets on this site" as its own card between the Trac ticket
card and the patch card:

(screenshot: ticket-linked)

With no ticket linked — "Your tickets on this site" as its own card; the Trac ticket card is
left with the field to type one into:

(screenshot: no-ticket-linked)

juanmaguitar and others added 2 commits August 10, 2026 15:07
Closes #240.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…alls not names

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@juanmaguitar
juanmaguitar merged commit 772d6c5 into trunk Aug 10, 2026
3 checks passed
juanmaguitar added a commit that referenced this pull request Aug 10, 2026
)

## Why

A linked pull request's state renders as grey text, the same size and
weight as the date beside it
and separated from it by a middle dot: `closed · updated 08/08/2026`.
The one word that says what
happened to the work reads as part of the timestamp, so the panel
answers "when was this touched"
loudly and "did this go anywhere" in a whisper.

Part of #227 — its "make the outcome legible" half.

## What changes

The state gets the pill the "Latest" marker already uses, in GitHub's
own three colours: green for
open, purple for merged, red for closed. Same shape, same size, same
radius — only the word and the
colours differ, so the row keeps one visual vocabulary.

Merged is a third state the parser used to throw away. GitHub's `state`
field only ever says open or
closed; a merge is visible solely in `pull_request.merged_at`, which the
`search/issues` response the
app already fetches carries. Keeping the distinction therefore costs
**no extra request** against the
shared unauthenticated 60/hour that `src/patch-sources.cjs` is careful
with.

Three constraints the design is built under, all three load-bearing:

- **The colour accompanies the word, it never replaces it.** A bare
coloured dot would tell a
contributor who cannot separate red from green *less* than the plain
text it replaced.
- **The closed pill must not read as an error.** Red means "something
failed" everywhere else in this
window — a half-done update, an error banner. A closed pull request is
not a failure. So the pill
wears GitHub's red (`#ffebe9` / `#82071e`) rather than this app's error
pair (`#fcf0f1` on a
`#d63638` border), and it is a borderless pill rather than the bordered,
alert-shaped box the
  errors use.
- **No copy is added.** The pill says `open`, `merged` or `closed` and
nothing else.

The mapping lives in a new `src/renderer/pr-state.cjs` with its own
test, per the renderer invariant
in the review standard; `index.jsx` is left holding the JSX and the
call.

**Deliberately not in this PR:** anything about what a state *means*.
This does not close #227 — its
other half, telling a pull request whose work landed from one that was
abandoned, needs the ticket's
own state and is not solved by colour.

## How to test this

**Platforms:** any. Nothing here touches paths, spawning, line endings
or signing.

**Starting state:** any initialized site, past the wizard. Needs network
— the list comes from
GitHub's search API.

1. In **Trac ticket**, type `29798` and click **Link ticket**. (Chosen
because it has one open and
   one closed linked pull request; any ticket with linked PRs works.)
→ Under **Linked pull requests**, `#12943` shows a red `CLOSED` pill and
`#8913` a green `OPEN`
pill, each on the line under the title, with `updated <date>` in grey
beside it.
2. Read the two pills next to the error styling elsewhere in the app —
e.g. trigger any error
banner, or compare against the yellow rate-limit notice on the same
card.
→ The `CLOSED` pill is visibly a label, not a warning: no border, no
icon, a softer red than the
     bordered `#d63638` alert boxes.
3. Turn off the network and click **Refresh**.
→ The cached list still renders, still with its pills. A row whose state
the cache does not
recognise renders as `OPEN`, which is what the row has always done —
never blank, never
     unlabelled.

**The merged/purple case cannot be reached against wordpress-develop.**
A repository-wide search
returns zero merged pull requests ever, against roughly ten thousand
closed — pull requests there are
opened for review and the change lands as a commit. To see it, force it
locally: in
`src/patch-sources.cjs`, make `prState` return `'merged'` for a closed
item, restart, and the row
shows the purple `MERGED` pill. It is covered by unit tests either way.

**What must not have happened:**

- The word must never disappear. Colour is added to the label, not
substituted for it — a pill with
  a colour and no text is the regression to watch for.
- The list itself must not change: same pull requests, same order
(newest first), same "Latest"
  marker, same **Apply…** behaviour. Only the state's rendering moved.
- No second GitHub request. The rate-limit budget is unchanged;
`merged_at` comes from the response
  already being parsed.

## Risks and limitations

- Presentation only. It does not close #227.
- The purple/merged branch is real in the API and correct to implement,
but it will essentially never
render for wordpress-develop (see above). It is unit-tested rather than
screenshotted from the real
  repo.
- After a GitHub failure the row falls back to the cached list, so a
pull request merged since the
last successful fetch keeps its old pill until the next successful
lookup. The cache is already
labelled "showing what was last seen …", so this is visible rather than
silent.
- Self-review: **0 [fix here] · 1 [follow-up]**, deferred with a reason
below.

## Related

Part of #227. Stacked on #242.

---

<details>
<summary>Design decisions and alternatives considered</summary>

**Why a pill and not coloured text.** Coloured text at 11px next to a
grey date is a contrast
problem before it is a legibility win, and it keeps the state looking
like part of the timestamp
sentence. The pill separates the two: a label and a date, not one run of
small print. `latestPill`
already established the shape in this exact row, so following it was
cheaper than inventing one — the
two now share a `pillStyle` object rather than two copies of the same
eight properties.

**Why GitHub's colours and not the app's palette.** The contributor's
next click on that row is
through to GitHub, where the same three states are painted the same
three ways. Borrowing an
in-house green would mean the same fact wears two different colours a
second apart.

**Why red for closed, given red means failure here.** The alternative
was grey — but grey is what the
state looks like today, and it collapses closed and merged into "not
open", which is exactly the
distinction the parser change exists to keep. GitHub's red is a
different red from this app's error
red, and the pill shape carries no alert affordance: no border, no icon,
no `role="alert"`. A test
pins the two apart.

**Why `merged` rather than a boolean `merged` flag on the closed
state.** Three states is what the UI
renders and what GitHub itself models. A `{state, merged}` pair would
push the same three-way
decision into every consumer.

**Unknown states read as open.** `prStateBadge` falls back to the open
styling for anything it does
not recognise — a list cached by an older build carries only
`open`/`closed`, and "not closed reads
as open" is the contract the row has always had. Failing to a blank or
dropped label would be worse
than failing to the old behaviour.

</details>

<details>
<summary>Review outcome (required — see AGENTS.md)</summary>

`/self-review` (the procedure in
`.github/instructions/code-review.instructions.md`, judgement pass
in a fresh context): **0 [fix here] · 1 [follow-up] — deferred.**

Deterministic layer clean: `npm run lint` clean, `npm test` 762/762 on
`.nvmrc`'s Node 24.18.0,
`npm run test:electron` 762/762 on Electron's bundled Node.

The follow-up, **tests · 🔵 low**, on `test/pr-state.test.cjs`: two
assertions read `index.jsx`'s
source text rather than behaviour, so an unrelated change could turn
this file red —
`assert.ok(source.includes('#fcf0f1'))` couples it to the error-banner
background, and the
exactly-one-`prStatePill(`-call-site count would fail if a second,
legitimate call site were added.

Deferred, consciously:

- The call-site count is the house pattern, adopted one commit ago in
`test/ticket-branch-list.test.cjs`
for the same reason — counting calls rather than bare names so a comment
naming the helper is not a
  red suite. Departing from it here would leave two conventions.
- The `#fcf0f1` assertion is a deliberate canary with its failure
message written for exactly this
case ("the error banner background moved; this test no longer compares
against the real one").
Without it, the "closed is not the error styling" comparison rots
silently the day the banner is
  restyled — which is the more expensive failure of the two.

Both are cheap to revisit if the second call site or the restyle
actually arrives.

</details>

<details>
<summary>Implementation notes</summary>

`src/patch-sources.cjs` — `parseLinkedPrs` calls a new `prState(item)`:
`merged` when
`pull_request.merged_at` is set, else `closed`/`open` from `item.state`.
The `pull_request` guard the
function already had (it is what tells a PR from an issue in a
`search/issues` response) means the
property access cannot throw on an issue item. `merged_at` presence in
this response was verified
against the live API.

`src/renderer/pr-state.cjs` — new: `PR_STATE_BADGES` (label + background
+ colour per state) and
`prStateBadge(state)`, case-insensitive with the open fallback. No
React, no DOM; testable.

`src/renderer/index.jsx` — `pillStyle` extracted from `latestPill`; new
`prStatePill(state)` beside
it; the PR row's second line becomes a flex row of pill + `updated
<date>` instead of a single
dot-joined string.

Tests: `test/patch-sources.test.cjs` gains the
merged/closed-unmerged/open trio;
`test/pr-state.test.cjs` is new. Both halves were verified to fail
without the source change —
`parseLinkedPrs: a merged PR is merged, not closed` fails on the old
parser, and
`the row renders the state through the pill` fails on the old
`index.jsx`.

No new dependency, no IPC surface, no persistence-shape change, no
platform-split code.

</details>

<details>
<summary>Screenshots or recording</summary>

Ticket #29798 on a real site, which has one closed and one open linked
pull request.

**Before** — the state is grey 11px text, dot-joined to the date:

`#12943 … ` / `closed · updated 08/08/2026`
`#8913 … ` / `open · updated 06/07/2026`

**After** — a red `CLOSED` pill and a green `OPEN` pill, each followed
by `updated <date>` in grey.

**Merged**, staged locally by forcing `prState` to return `'merged'`
(wordpress-develop has no merged
pull requests to screenshot): the same row with a purple `MERGED` pill.

</details>

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
juanmaguitar added a commit that referenced this pull request Aug 10, 2026
…es that predate it (#245)

> [!WARNING]
> **Do not merge this before the ticket-branches stack (#168 #185 #198
#205 #218 #238 #241 #242 #243).** It documents features that are not on
trunk yet. Merged first, the published guide describes an app nobody can
download.

## Why

The user guide describes a site as one working tree and one implicit
patch, because that is what the app was when the guide was written. The
ticket-branches stack makes a ticket a branch on the site, which leaves
several pages not merely incomplete but wrong: what a patch contains,
what **Unlink** costs you, what a trunk update does to work in progress,
whether the applied-patch banner describes the ticket you are looking
at.

The stack also adds surface nothing documents at all — the tickets card,
the question asked about edits made before a ticket was picked, the note
about unsubmitted work, the switch progress line, coloured pull request
states.

## What changes

Docs only. The diff is `docs/` — guide pages,
`docs/.vitepress/config.mjs` for the sidebar, and screenshots. No
`src/`, no `scripts/`.

**New page: `guide/ticket-branches.md`, "Working on several tickets".**
The model (a site is the expensive substrate, a ticket is a cheap
branch), starting a second ticket, the tickets card in both its states,
what a switch says while it runs and what to do when one fails part-way,
deleting one ticket's work, the four-choice question about loose trunk
edits, the unsubmitted-work note, and what a ticket's patch does and
does not contain. Added to the sidebar under "Contributing changes",
after "Working on a Trac ticket".

**Corrections to pages that describe the old model:**

| Page | What was wrong |
| --- | --- |
| `trac-tickets.md` | Unlink described as only forgetting an
association; no mention that a site holds many tickets, that linking
asks about loose edits, or that PR states are now coloured pills with a
third value |
| `submitting-changes.md` | "everything this site has that its copy of
trunk does not" — the diff is now the ticket's own work, measured from
its branch point. Also gained what a patch can and cannot carry:
deletions now travel, binaries and unreadable files are named above the
diff rather than dropped |
| `trunk-updates.md` | Said nothing about being on a ticket — that the
update parks and returns, and that the branch point deliberately does
not move |
| `applying-patches.md` | Applied-patch state is per ticket now; and
"use Update to latest trunk to reset the checkout" does not hold on a
ticket branch, where the update puts you back exactly where you were |
| `creating-a-site.md` | Implied a site per piece of work |
| `managing-sites.md` | **Delete this site** takes every ticket's work
with it; and its dirty-tree dialog is about loose edits only, never
parked ticket work |
| `troubleshooting.md` | New entry for the refused-switch message, which
is what someone will search for |
| `getting-started.md`, `setup-wizard.md`, `submit-*.md`,
`running-the-site.md` | The button is **Review & submit changes**; the
guide still quoted **Submit changes** everywhere |

**Deliberately not in this PR:** new entries in
`scripts/screenshots/shots.cjs` for the panels the stack adds. A shot
definition belongs with the code that draws the panel, and one added
here would fail on trunk where the panel does not exist — see Risks.

## How to test this

**Platforms:** any. This is a static site; nothing here touches the app.

**Starting state:** this branch checked out.

1. `npm run docs:build` → completes. This is the real check:
`ignoreDeadLinks: false`, so a broken link or a missing image fails it.
2. `npm run docs:dev` and open `/guide/ticket-branches` → the page
renders, the six screenshots load, and "Working on several tickets" is
in the sidebar under **Contributing changes**, between "Working on a
Trac ticket" and "Applying patches and PRs".
3. Follow the internal links out of that page —
`#what-a-patch-contains`, `#edits-you-made-before-picking-a-ticket`,
`#deleting-a-ticket-s-work`,
`trunk-updates#updating-while-you-are-on-a-ticket`,
`submitting-changes#what-a-patch-can-and-cannot-carry` → each lands on
the heading it names, not at the top of the page.
4. `npm run lint` → clean.

**And the part a build cannot check** — drive the stack (the Buildkite
artifact for `juanmaguitar/pr-state-colours` exercises all nine PRs)
with the new page open, and check the prose against the app:

5. On a site with two tickets, read "Starting a second ticket" and
follow it literally. → Unlink, then type the second number. The page
should not describe a control that is not on screen.
6. Read the switch progress sentences in the page against the ones the
panel actually shows.
7. Compare the six screenshots against the panels as they render for
you.

**What must not have happened:**

- **No `src/` or `scripts/` file in the diff.** `git diff --stat
origin/trunk...HEAD` must list only `docs/`. If a screenshot-harness
change slipped in, it will fail on trunk.
- **No invented UI string.** Every label quoted in these pages was read
out of the stack's source or seen in the app. A plausible-sounding
button that does not exist is worse than no documentation.
- **No page left describing the old model.** The table above is the
list; if another page still says a site carries one ticket, it was
missed.

## Risks and limitations

**This PR must merge after the stack.** It is the only real risk here
and it is not defended by anything automated: nothing in CI knows the
difference. The Pages deploy runs on trunk, so merging early publishes
it immediately.

- **`scripts/screenshots/shots.cjs` has no entries for the new panels**,
so the six new images are not reproducible with `npm run shots` as
things stand. They were taken by driving the stack through the same
Playwright harness in a throwaway worktree, with fixture and shot
definitions that were not committed anywhere. Follow-up, and it belongs
on a branch that has the panels: the fixtures need real repositories
with ticket branches, where the current ones are empty directories.
**Follow-up issue not yet filed** — happy to file it.
- **`site-view.png` is re-shot** and is the only existing image
replaced. Its button still read **Submit changes**, which was already
stale on trunk after #235 renamed it — so that image and the text
corrections around it are true of trunk today, stack or no stack.
- **The `MERGED` pill is documented but not screenshotted.** A
repository-wide search returns no merged pull requests on
`wordpress-develop`; the state is real in the API and unit-tested, and
#243 says the same.
- **The screenshots name a real ticket and its real pull requests**
(#29798), pulled live from GitHub when the images were taken. They will
age the way any screenshot of live data ages.
- Review outcome: **5 [fix here] · 1 [follow-up] — all 6 addressed.**
Every one was a factual claim about the app, not prose. Details in the
collapsed block.

## Related

Documents #168, #185, #198, #205, #218, #238, #241, #242, #243 — the
ticket-branches stack, tip branch `juanmaguitar/pr-state-colours`. Part
of the contribution-flow tracker #110. Follows the docs site (#230), its
screenshot harness (#231) and the guide itself (#232).

---

<details>
<summary>Design decisions and alternatives considered</summary>

**A new page rather than growing `trac-tickets.md`.** That page is about
one ticket: linking it, and reading the work that already exists on it.
Several tickets is a different subject with its own vocabulary —
parking, switching, deleting a branch — and folding it in would have
doubled the length of a page a first-time contributor reads on their
first ticket, to describe something they do not do yet.
`trac-tickets.md` gains pointers instead.

**Corrections before additions.** A page that is wrong costs more than a
page that is missing: someone acts on it. So the pass over the existing
guide came first, and the new page was written to fill what was left.

**Screenshots taken by hand rather than by adding shot definitions.**
The harness lives on trunk and the panels live on the stack, so a shot
definition committed here would fail every `npm run shots` on trunk
until the stack lands. The images were taken by assembling the two in a
scratch worktree — stack tip, plus trunk's `scripts/screenshots/` and
the `TOOLKIT_USER_DATA_DIR` hook, plus fixtures that build real
repositories with ticket branches — and only the resulting PNGs were
copied here. That worktree is gone; nothing of it is in this diff.

**Quoting the app rather than paraphrasing it.** Where a sentence in the
app is the thing being explained — the question about loose edits, the
switch stages, the confirm before a delete — the page quotes it
verbatim, so someone reading with the app open can match what they see.
Every quote was read out of the stack's source.

</details>

<details>
<summary>Review outcome (5 [fix here] · 1 [follow-up] — all
addressed)</summary>

Run per `.github/instructions/code-review.instructions.md`.
Deterministic layer inline and clean — `npm run lint` clean, `npm test`
638/638, `npm run docs:build` clean. The judgement pass went to a
subagent with the diff, the instructions file and the stack's source,
and nothing from the session that wrote the pages; it was asked to check
every quoted string and every behavioural claim against the code rather
than to read the prose.

Every finding was an accuracy finding — the guide asserting something
the app does not do. All five were verified against the source before
being fixed.

| # | What was wrong |
| --- | --- |
| 1 🔴 | The new page's central how-to. "Type the number into the **Trac
ticket** panel" — the panel holds no input while a ticket is linked; the
field is in the unlinked branch only. The route is **Unlink**, then link
the new number. Unfollowable as written. |
| 2 🟡 | The mid-switch advice described a recovery that does not exist:
the marker is written only when a checkout throws inside a running app,
so a force-quit leaves nothing behind, and `midSwitchBlock` refuses the
retry along with everything else. The one action it allows is
**Unlink**, which is now what the page and `troubleshooting.md` say. |
| 3 🟡 | `applying-patches.md`'s "use **Update to latest trunk** to reset
the checkout" escape hatch. On a ticket the update parks, resets trunk
and checks the branch back out — applied patch included — so it leaves
you where you started. |
| 4 🟡 | "Nothing is rebuilt" was true and misleading: a switch does not
rebuild, so a running dev server keeps serving the previous ticket's
assets. The page now says to run `npm run build` after a switch, which
is what the site view itself says. |
| 5 🔵 | "If the note says two changes, the patch has two files." The
note counts everything the patch speaks about, binaries included, and
those are named above the diff rather than carried — so one text file
and one image is two in the note and one in the diff. It also
contradicted this PR's own `submitting-changes.md`. |
| 6 🔵 | The by-hand replay recipe skipped the trunk update that gives
the new branch a newer base, and offered **Delete this ticket's work**
for the ticket you are on, which the card never lists. |

**`[follow-up]`, both taken here rather than deferred** because they
were two sentences each: `managing-sites.md` and `trunk-updates.md`
described the same dirty-tree dialog differently, and
`troubleshooting.md` had no entry for the refused-switch message.

Style notes, also applied: the tickets card is additionally gated on the
setup checklist being finished or skipped, and the loose-edits question
can be raised from the **Attach to Trac** card's link field too.

The pass also confirmed what was most likely to be wrong and was not:
every other quoted label and message is verbatim, all six screenshots
match their alt text, `#deleting-a-ticket-s-work` resolves under
VitePress's slugifier, and the new page is in the sidebar.

</details>

<details>
<summary>Implementation notes</summary>

**How the screenshots were taken.** `git worktree add` from the stack
tip, then `git checkout <trunk> -- scripts/screenshots docs/…`,
`playwright-core` installed, and the `TOOLKIT_USER_DATA_DIR` hook from
`src/main.js` re-applied — the stack predates all of it, so `npm run
shots` does not exist on that branch. Two throwaway fixture variants
were added, both building real repositories through the app's own
`src/ticket-branches.js` (`startTicketBranch`, `parkCurrentWork`,
`switchToBranch`) so the state is the state the app makes: one site on
`ticket/29798` with parked work and a second branch, one parked back on
trunk with a loose edit. Same 1200×800 window and
`--force-device-scale-factor=1` as `capture.cjs`, so the new images sit
beside the existing ones without a size jump. The worktree has been
removed.

**The new slugs**, none of which `shots.cjs` knows about:
`ticket-list-card`, `ticket-list-unlinked`, `trunk-work-question`,
`carried-work-notice`, `linked-pull-requests`, `site-with-tickets`.

**`linked-pull-requests.png` is one image doing three jobs** — the
coloured state pills (#243), the unsubmitted-changes note (#241), and
the reorganised ticket card with the tickets list no longer inside it
(#242).

**`docs/` is its own npm package**, so `npm run docs:build` needs `npm
--prefix docs ci` first on a fresh checkout.

</details>

<details>
<summary>Screenshots or recording</summary>

Six new images, and one replaced.

**The tickets card, linked** — `ticket-list-card.png`:

![Other tickets on this
site](https://raw.githubusercontent.com/WordPress/contributor-toolkit/juanmaguitar/docs-ticket-branches/docs/public/screenshots/ticket-list-card.png)

**The tickets card, unlinked** — `ticket-list-unlinked.png`:

![Your tickets on this
site](https://raw.githubusercontent.com/WordPress/contributor-toolkit/juanmaguitar/docs-ticket-branches/docs/public/screenshots/ticket-list-unlinked.png)

**The question about loose trunk edits** — `trunk-work-question.png`:

![You have 1 uncommitted change on this site, not on any ticket yet.
What should happen to
them?](https://raw.githubusercontent.com/WordPress/contributor-toolkit/juanmaguitar/docs-ticket-branches/docs/public/screenshots/trunk-work-question.png)

**After choosing to carry them** — `carried-work-notice.png`:

![Your 1 uncommitted change came along into #62281, and will go into its
patch.](https://raw.githubusercontent.com/WordPress/contributor-toolkit/juanmaguitar/docs-ticket-branches/docs/public/screenshots/carried-work-notice.png)

**The ticket card with coloured pull request states and the
unsubmitted-work note** — `linked-pull-requests.png`:

![A red CLOSED pill and a green OPEN pill on the two linked pull
requests](https://raw.githubusercontent.com/WordPress/contributor-toolkit/juanmaguitar/docs-ticket-branches/docs/public/screenshots/linked-pull-requests.png)

**The three cards in their new order** — `site-with-tickets.png`:

![Trac ticket, then Other tickets on this site, then Apply a patch or
PR](https://raw.githubusercontent.com/WordPress/contributor-toolkit/juanmaguitar/docs-ticket-branches/docs/public/screenshots/site-with-tickets.png)

**Replaced:** `site-view.png`, whose action bar read **Submit changes**.
No stack behaviour is visible in it — it is a straight re-take of a
stale image.

</details>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@juanmaguitar
juanmaguitar deleted the juanmaguitar/tickets-own-card branch August 11, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A site's tickets deserve their own card, not a sub-heading inside the ticket you are on

1 participant