feat(panel): deep links to review threads in the Pull request tab

Merged
DA
10d ago
+1384-194

Deep links for review comments in an agent's Pull request tab: press a thread in the Conversation to open it in Files changed, and back again. Each thread also gets a Luke link of its own.

What it does

A link place for one thread. It's LINK_PLACE.PULL_REQUEST_THREAD { planId, agentId, threadId, view }, written under the agent's Changes link:

https://tryluke.dev/<ws>/plan/<planId>/agent/<agentId>/changes/conversation/thread/<threadId>
https://tryluke.dev/<ws>/plan/<planId>/agent/<agentId>/changes/files/thread/<threadId>

threadId is GitHub's GraphQL node id for the thread (ReviewThread.id), the same id reply and resolve already use. Opening the link:

  1. openLink (use-tasks-tab) opens the agent's Pull request tab, reusing a shared tab the way CHANGES does, on the view the link names.
  2. It leaves a counted threadReveal ask on a context (thread-reveal.tsx), the same pattern as MessageReveal.
  3. The tab takes the ask, finds the thread in its conversation read, and brings it into view. If the read no longer holds that thread, a toast says so.

Both in-tab jumps call openPlace with this place, so they take the same route as a link from anywhere. Copy link in a thread's ⋯ copies the link for the view the thread is drawn in, through useCopyLinkItem.

Conversation → Files changed. Pressing a thread's file/lines header, or ⋯ › View in Files changed:

  • opens the file if it is collapsed or folded as viewed (new ChangesList.expand), and clears a filter that hides it
  • shows a minimized line's chip and opens the thread, even a resolved or outdated one
  • scrolls Pierre's list to the thread's line (new DiffListHandle.revealLine, using CodeView.scrollTo({ type: "line" }))
  • for a line no hunk shows, or an outdated thread, scrolls to the file header, where the above-the-diff list stands
  • lights the thread's lines in the lit range from #3818 until its ring ends
  • rings the card (data-search-landed) and focuses its summary with preventScroll

A thread whose file is no longer in the diff gets a toast saying so, and is shown in the Conversation instead.

Files changed → Conversation. ⋯ › View in Conversation scrolls the Conversation's own scroller to the card (centreWithin, never scrollIntoView), then opens, rings and focuses it. A thread brought into view stays visible even if Hide resolved or Hide bots would hide it.

How the scroll stays right while things expand

The jump is a pending state that resolves over renders, not in one call:

  1. useListLanding opens the file, shows the chip, and records the place.
  2. PierreDiffList's reveal effect runs after its update effect, so Pierre already has the file open and its notes laid out. Only then does it call revealLine or reveal.
  3. Pierre keeps the target pending and re-resolves it every frame, and on every measurement, until it settles. Files above the thread that mount and measure taller on the way still land it in the right place.

The card's focus and ring wait, through focusSeek, until the card stands inside the list's box.

Screenshots

LightDark
Conversation (press src/expiry.ts lines 34–35)
→ Files changed: file opened, scrolled to line 35, lines lit, thread ringed and focused
⋯ › View in Conversation: the card centred, ringed and focused
A thread's file and lines under the pointer: underlined, with the tooltip "View in Files changed"

Tests

  • luke-link.test: both views round-trip, plus refusals for a bad view, a misspelled step, a missing or trailing id, a taskless agent, and an id with characters outside the pattern.
  • use-tasks-tab.test: the route opens the Pull request tab, sets the view, and leaves counted asks; letting go of an older ask leaves the newer one standing.
  • agent-pull-request-tab.test:
    • a collapsed file plus a minimized thread, reached through the window's route: the file opens, the list scrolls to line 13, the lines light, the thread is ringed and focused, and the lines go out as the ring ends
    • a line outside the hunks, and an outdated thread: the list scrolls to the file header, and the folded thread opens above the diff
    • a file no longer in the diff: the thread stays in the Conversation and is shown there
    • Files changed → Conversation through Hide resolved
    • Copy link

Checks

  • pnpm check:affected ran out of memory on this sandbox (exit 137 on the service and web typecheck, and features workers timing out). I ran the fallback set instead, and it passed:
    • tsc for features, panel, desktop, ui and web
    • features vitest and the touched panel suites
    • ./scripts/oxlint.sh, biome
    • pnpm knip
    • ./scripts/repository-checks.sh
  • Desktop/panel and @luke/features/luke-link only, with no service change, so no Preview run is needed.

The stacked follow-up will open GitHub anchors (#discussion_r…, #issuecomment-…, #pullrequestreview-…) inside the tab.

Made with Luke