Give a site's tickets their own card between the ticket and the patch panels - #242
Merged
juanmaguitar merged 3 commits intoAug 10, 2026
Conversation
Closes #240. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…alls not names Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…guitar/tickets-own-card
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`:  **The tickets card, unlinked** — `ticket-list-unlinked.png`:  **The question about loose trunk edits** — `trunk-work-question.png`:  **After choosing to carry them** — `carried-work-notice.png`:  **The ticket card with coloured pull request states and the unsubmitted-work note** — `linked-pull-requests.png`:  **The three cards in their new order** — `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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
ticketActionsBlockeddisabling and the orderingare byte-identical —
renderBranchRowsmoved, it did not change. What is new is one exportedhelper,
ticketListCardinsrc/renderer/ticket-branch-list.cjs, holding the two decisions thecard 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 progressline, 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
→ 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.
→ The switch runs exactly as before. Nothing about the row behaviour changed.
→ Every switch link and every Delete this ticket's work button is disabled, and comes
back when the operation ends.
→ Same confirm wording as before, nothing deleted.
With no ticket linked
→ 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.
→ 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
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 same ticket offered in two places, that is the bug this change could most easily have
introduced.
same card, with only the heading changing from "Other tickets" to "Your tickets".
card should disappear along with the last row, not linger as a heading.
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: 24column, so the panel lands directly above the row youclicked, 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. Twoliterals 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
ticketListCardreturns null rather than the JSX deciding. The "no rows, no card" rule isthe 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.jsxas text and asserts the ordering of the three card headings and thatrenderBranchRows(is called exactly once. It is not a substitute for looking at the app, and itis 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 asubagent that had the diff and the instructions file and nothing from the session that wrote the
code.
Fixed:
fontSize: 16to pin the headinglevel.
fontSize: 16is repeated verbatim on all three card headings, so extracting them into ashared 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.
renderBranchRows, whichincludes 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:
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.
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
renderBranchRowsdefinition, one call site,Boolean(tracTicket)reproducing the oldlinkedargument in both states);ticketActionsBlockedstill closed over from the same component; the empty-list guard;
skipInitgating matching the twoneighbouring cards exactly. Security, performance and cross-platform are out of scope — no IPC, no
spawn, no path handling, no dependency, no
electron-storeshape change.Implementation notes
Three files.
src/renderer/ticket-branch-list.cjs— addsticketListCard({ rowCount, linked }), returning{ heading }ornull. Pure and dependency-free like the rest of the module, sonode --testrequires it directly.
src/renderer/index.jsx— deletes the two mutually exclusive sections (the linked one at the topof 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.renderBranchRowsitself isuntouched.
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 test754/754 on Node 24.18.0 (.nvmrc) and 754/754 on Electron's bundled Node vianpm run test:electron, both re-run after the rebase onto #241.npm run lintclean.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)