feat(web): open GitHub references from pull request bodies - #134
Merged
Conversation
yngatech-nightly
Bot
force-pushed
the
main
branch
from
August 14, 2026 17:07
6441a30 to
96ee4ad
Compare
incognitojam
force-pushed
the
t3code/render-github-reference-links
branch
2 times, most recently
from
August 14, 2026 17:57
5567551 to
6fe2ae4
Compare
`#123`, `GH-123` and `owner/repo#123` name an issue or a pull request, and a bare URL to one is written back as that shorthand. Neither is GFM, so remark left both as they were. The link is addressed at `/issues/{n}` whichever kind it turns out to be: the host redirects that to `/pull/{n}` for a pull request, so the link is right in a browser before anything is known about it. Inert without a repository to read a number against, which is what keeps `#2` in a conversation the plain text it reads as. Nothing passes one yet. The matching rules are GitHub's own, each checked against its renderer: `foo#123` and `#123abc` are not references, `(#123)`, `#123.`, `#123-abc` and `/#123` are, `#0` is not, and code spans and link labels are left alone. The body is read twice — as a tree to linkify, as text to collect what to ask about — so the two are held to an invariant rather than to each other: asking covers everything rendered, since over-asking costs one entry in a batch while under-asking would leave a rendered reference unanswered for good. A test holds that containment over the bodies where they disagree. The marker travels as a data attribute, so the sanitizer's allowlist is part of the feature: one missing from it is stripped silently, and every test that stops before the rehype half keeps passing while nothing renders at all.
A body can name a dozen issues and pull requests, and a request each would be a dozen processes against a host whose limits are low. One GraphQL document asks about them together: repositories become aliased fields, numbers become aliased fields inside them, and `issueOrPullRequest` says which of the two a number turned out to be without having to guess first. An answer is only reported when the host was clear about it. `gh` exits non-zero whenever any part of an answer failed while still printing the parts that resolved, so the body is read for itself and the exit code only decides what an *empty* body meant — a rate-limited or logged-out host must not read as a body full of references that do not exist. For the same reason a null is read as nothing-there only where the host filed `NOT_FOUND` against it: `FORBIDDEN` is SAML, an IP allowlist or a token scoped elsewhere, about a reference the reader may well be able to open themselves. The hostname comes from the checkout's own remotes rather than from the caller: it is where credentials get sent, which is not a decision to hand to a body's own text. The answer carries that host back, because a reference is keyed by repository and number, which an Enterprise install and github.com spell identically. Repository paths travel as GraphQL variables over stdin. One cache keeps a panel of many bodies from spawning a process per body for numbers a neighbour just resolved; it takes its clock as an argument so its expiry, its two lifetimes and its bound are testable. Nothing calls this yet.
The pull request surfaces now read `#123`, `GH-123` and `owner/repo#123` in every body they render — the description, comments, reviews and timeline — and follow them the way the app follows anything else it recognises. What each number turns out to be decides where it opens: a pull request in a project on this machine opens as a tab beside the one being read, and an issue or another organisation's repository opens in a browser, since there is no issue surface here to open it in. Cmd/Ctrl+click is the browser either way. A reference clicked before its answer arrives follows the link as written, which the host redirects to whichever kind it is — the right page, in a browser. A number the host has nothing under is underlined the way an unknown word is, and still opens: the reader may have access in a browser they are signed into differently. Only an answer marks a link this way. A request that failed on the way, or one answered by a different host than the body was written against, leaves every reference alone — a host having a bad minute cannot repaint a body full of good references as mistakes. That mark is a rule rather than a class, because `.chat-markdown a` sets `text-decoration: none` and outranks any single class put on the element. The thread a panel is mounted beside reaches the bodies it renders through one context, rather than a prop threaded down through the tabs, none of which has any other use for it. It is provided from outside the panel's own body, so the panel reads exactly as it did before and only reference clicks are affected.
incognitojam
force-pushed
the
t3code/render-github-reference-links
branch
from
August 14, 2026 18:19
6fe2ae4 to
7101e5f
Compare
incognitojam
enabled auto-merge (squash)
August 14, 2026 18:22
incognitojam
added a commit
that referenced
this pull request
Aug 15, 2026
> [!NOTE] > `#123`, `GH-123` and `owner/repo#123` in a pull request body are now links, the way they are on > GitHub. What each number turns out to be decides where it opens: a pull request in a project on > this machine opens as a tab beside the one being read, an issue opens in a browser, and a number > the host has nothing under is underlined in red and still opens. GitHub's autolinked references are its own extension rather than GFM, so remark left them as they were: every `pingdotgg#6039` an agent or a reviewer wrote in a description, a comment or a review stayed plain text, and following one meant retyping it into a browser. Reading them is the easy half. Knowing what one *is* takes asking the host, and a body can name a dozen — so they are asked about together, in one GraphQL document, aliased by repository and by number. `issueOrPullRequest` answers which of the two each number turned out to be, which is what decides where it opens; a cache keeps a panel of many bodies from spawning a process per body for numbers a neighbour just resolved. The link is addressed at `/issues/{n}` before any of that comes back, because the host redirects that to `/pull/{n}` for a pull request — so it is already right in a browser, and resolving only ever improves it. A reference clicked before its answer arrives follows it as written. Everything else follows from one rule: **only an answer marks a link as broken.** A request that failed on the way — rate limited, logged out, offline — leaves every reference exactly as it was, and a null is read as nothing-there only where the host filed `NOT_FOUND` against it, since `FORBIDDEN` is SAML or an IP allowlist standing between the reader and something they can very likely open themselves. A host having a bad minute must not repaint a body full of good references as mistakes. Inert unless a surface passes the repository to read numbers against, so `#2` in a conversation — where it is far more likely to be a step than an issue — stays the plain text it reads as. The description of #126, which cites the stale-stack guard from `#95` and the window it was built for (`#94`). | Before | After | | --- | --- | | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/github-reference-links/before.png" alt="Reference numbers rendered as plain grey text" width="460"> | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/github-reference-links/after.png" alt="The same numbers rendered as links" width="460"> | `#95` resolves to a pull request and opens as a tab here; `#94` resolves to an issue and opens in a browser, since there is no issue surface to open it in. A reference the host has nothing under keeps its link colour and takes the mark an unknown word takes. No pull request in this repository happens to cite a number the repository lacks, so the state below was forced on a resolved reference to photograph it: <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/github-reference-links/missing.png" alt="A reference underlined with a red wavy line" width="460"> Three layers, each of which stands alone: the plugin that reads references and renders nothing differently on its own, the request that answers them and is called by nobody, then the wiring that turns both on. - `vp test run` for the touched suites: `sourceControl` (147), `GitManager`, and the web `markdown-github-references`, `chat` and `pullRequest` suites (469). - Targeted `tsgo --noEmit` for `apps/web`, `apps/server` and `packages/contracts`, at each of the three commits rather than only at the tip. - The matching rules are GitHub's own, each checked against its renderer before being written down: `foo#123` and `#123abc` are not references, `(#123)`, `#123.`, `#123-abc` and `/#123` are, `#0` is not, and code spans and link labels are left alone. - Read live against this repository in `test-t3-app`: references linkified in a real description, `#95` re-addressed itself to `/pull/95` and opened here, `#94` stayed addressed for the browser. That pass is also what found two faults nothing else did — a memoized renderer holding the first render's answers, and a mark cancelled by `.chat-markdown a { text-decoration: none }`. --- Written by an agent (T3 Code, claude-opus-5).
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.
Note
#123,GH-123andowner/repo#123in a pull request body are now links, the way they are onGitHub. What each number turns out to be decides where it opens: a pull request in a project on
this machine opens as a tab beside the one being read, an issue opens in a browser, and a number
the host has nothing under is underlined in red and still opens.
GitHub's autolinked references are its own extension rather than GFM, so remark left them as they
were: every
#6039an agent or a reviewer wrote in a description, a comment or a review stayedplain text, and following one meant retyping it into a browser.
Reading them is the easy half. Knowing what one is takes asking the host, and a body can name a
dozen — so they are asked about together, in one GraphQL document, aliased by repository and by
number.
issueOrPullRequestanswers which of the two each number turned out to be, which is whatdecides where it opens; a cache keeps a panel of many bodies from spawning a process per body for
numbers a neighbour just resolved.
The link is addressed at
/issues/{n}before any of that comes back, because the host redirectsthat to
/pull/{n}for a pull request — so it is already right in a browser, and resolving onlyever improves it. A reference clicked before its answer arrives follows it as written.
Everything else follows from one rule: only an answer marks a link as broken. A request that
failed on the way — rate limited, logged out, offline — leaves every reference exactly as it was,
and a null is read as nothing-there only where the host filed
NOT_FOUNDagainst it, sinceFORBIDDENis SAML or an IP allowlist standing between the reader and something they can verylikely open themselves. A host having a bad minute must not repaint a body full of good references
as mistakes.
Inert unless a surface passes the repository to read numbers against, so
#2in a conversation —where it is far more likely to be a step than an issue — stays the plain text it reads as.
Before / after
The description of #126, which cites the stale-stack guard from
#95and the window it was builtfor (
#94).#95resolves to a pull request and opens as a tab here;#94resolves to an issue and opens in abrowser, since there is no issue surface to open it in.
A reference the host has nothing under keeps its link colour and takes the mark an unknown word
takes. No pull request in this repository happens to cite a number the repository lacks, so the
state below was forced on a resolved reference to photograph it:
Reading the commits
Three layers, each of which stands alone: the plugin that reads references and renders nothing
differently on its own, the request that answers them and is called by nobody, then the wiring that
turns both on.
Verification
vp test runfor the touched suites:sourceControl(147),GitManager, and the webmarkdown-github-references,chatandpullRequestsuites (469).tsgo --noEmitforapps/web,apps/serverandpackages/contracts, at each of thethree commits rather than only at the tip.
foo#123and#123abcare not references,(#123),#123.,#123-abcand/#123are,#0is not, and code spans and link labels are left alone.
test-t3-app: references linkified in a real description,#95re-addressed itself to/pull/95and opened here,#94stayed addressed for the browser.That pass is also what found two faults nothing else did — a memoized renderer holding the first
render's answers, and a mark cancelled by
.chat-markdown a { text-decoration: none }.Written by an agent (T3 Code, claude-opus-5).