Repository navigation
feat: read a linked pull request in Tcode (A4) - #664
Merged
Merged
Conversation
…les and media on the host
…pull requests from the client store
…est, thread hunks and avatars
…d diff list, Conversation and media
…s; test the diff list layouts
…age paragraphs across line breaks
…, so Windows builds the suite
… or date, reads stack layers the thread shows, and takes the node id from the REST read
…ad stops showing, revalidate media by its expiry, and count viewed files out of the pull request's
…'s line breaks reach the painted fragment
…il a read has landed
Tryanks
marked this pull request as ready for review
October 9, 2026 02:12
This was referenced Oct 9, 2026
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.
Summary
Closes #640 (accepted gaps: layer navigation not seen on a real stacked pull request, and the private-image, expired-signed-URL and account-switch cases fixture-only; see Gaps)
PullRequestReadsbehindReadCache(TTL, single-flight, failures kept for no one, byte budget, another account's answers dropped on read, invalidated by the sync when it observes a change and by RefreshPullRequest). The viewed mutation's node id comes from the RESTpulls/Nreadrevisions()already makes (no node-id map, no separate query). Transport deadline cap raised to 60 s for diff/file reads only; RESTAcceptoverride for the raw diff/file representations.changed_files, so a push that adds or removes files is picked up on the next read. A push that changes files without changing their count can still pair a files answer with the previous head for up to 60 s (the revisions TTL).Hunks | Binary | Oversized | Withheld, explicitcomplete+changed_files, review anchors on threads. A re-read of page 1 (expiry) keeps the pages read after it, the scroll andcompletewhile head, base and page 1 are unchanged; a moved head resets; a manual Refresh restarts from page 1. Never the local checkout.base.shainstead, which is the base branch's tip and can differ from what the diff shows. Deleted files are reconstructed from their patch alone; there is no base-revision read.completefalse past 10 pages. The Conversation count is comments plus reviews with a body.owner/repo/assets/N/id, blob/raw → raw host, raw/media hosts (credentialed); legacyuser-images,private-user-images, camo (no token); avatars only when an author in the conversation or a read reply has thatavatar_url— and answersUnsupportedfor anything else, which the client then draws by its URL. Token only on github.com / raw / media hosts and only for credentialed sources, never on a signed hop; ≤ 3 HTTPS redirects; size checked from Content-Length and while reading; decoded dimensions ≤ 8192 px / 24 MP; video/audio answerExternal. Revalidation: an ETag goes back as If-None-Match, a Last-Modified date as If-Modified-Since; 304 →NotModified.MediaOutcome(no strings in errors) behind its own GPUI asset, keyed by host connection + opaque account partition, 64 MiB budget. When an image'sexpires_atpasses the client drops the asset and asks again with its validator, drawing the old copy until the answer lands (NotModified keeps it). Failed loads are dropped on Refresh and whenever the conversation is read again on expiry. An account change (a conversation answered for another account) also drops the held files, viewed marks and replies and reads them again.Destination::PullRequest(not restored at launch). A failed file-text read shows the design's toast. Unlinking the pull request whose view is open (or the link whose stack shows it) closes the view back to the list, and the phone page pops back to the thread.DiffPanel's row rendering moved todiff/list.rsbehindDiffListHost; both views draw through it.Evidence
After: desktop light, PR tab Files with the file column on mbedTLS: Update to 4.1.0, PSA Crypto godotengine/godot#120725, 1200×800 pt, and phone light (
phone --local, 393×852), the PR page's Files view on the same pull request (synthetic profiles, English, this head):cargo fmt --all --checkclean ·cargo clippy --workspace --all-targets --locked -- -D warningsclean ·cargo nextest run --workspace --locked→1054 tests run: 1054 passed, 13 skipped·cargo machete→ no unused dependencies.crates/services/tests/github.rsimportedCredentialsat the top while only the#[cfg(unix)]gh test uses it after theStorehelper moved totests/support/github.rs→-D unused-importson Windows. Fixed by importing it inside that test.crates/services/tests/pull_request_reads.rs): whole diff with rename/deletion/binary + text at a revision; refused whole diff → pages, withheld/oversized patches, the original refusal when pages fail, and revisions read again when the files disagree withchanged_files(fails without the invalidation); outdated multiline thread + paged replies scoped to the PR; thread list capped at 10 pages; viewed state stops at 5 pages and marking re-reads only viewed state (paths and node id as variables, node id from the REST read); single-flight (the first answer is held until the other readers have asked), TTL via each answer'sexpires_at, failure not cached, account switch; media classification table, token only on credentialed GitHub hosts, public classes and avatars without token, an avatar admitted for a reply's author once the reply is read but not for a body quoting its URL, If-None-Match and If-Modified-Since revalidation (304 → NotModified), HTTP redirect refused, 3-redirect cap, declared over-cap, a chunked body without Content-Length cut at the cap while reading, oversized dimensions, non-media type, unmentioned URL,Unsupportedfor other hosts with no request sent.pull_requests::detail::tests::an_expiry_reread_keeps_the_pages_read_after_page_one_and_the_scroll— the view against a scripted host: page 1 lands expired, page 2 is read as the reader nears the end, the reader scrolls to file 120, then the expiry re-read of page 1 lands; 150 files,completeand the scroll stay (fails without the fix: 100 files, scroll reset). Markdown paragraph flowing around an image across CRLF breaks;markdown::selection_adapter::tests::text_wrapping_after_an_inline_image_paints_on_its_own_rows— excalidraw#8530's "Technical parts" paragraph (bold, CRLF, image, CRLF, long link) at 393 pt: no two painted text line boxes overlap (fails without the fix: the text after the image kept its CRLF, so its painted fragment dropped the link a row onto the link's wrapped tail). The flow now turns breaks into spaces once where it measures the text, for the wrapper, the shaper and the painted fragment alike. The refresh label reads "Refresh" until a files or conversation read has landed (was "updated 20735d ago").reads_are_shared_in_flight_…: the 15 s wall-clock sleep and its post-sleep assertions are deleted (step 1/2: a wall-clock wait proves nothing theexpires_atassertions do not; expiry is the cache'suntil, proven by each answer's expiry); the 100 ms overlap sleep is replaced by holding the first answer until the other readers have asked (step 1: drive concurrency deterministically).viewed_files_stop_at_five_pages_…: the exact alias-text assertion is deleted (step 2: the document text is an implementation detail; the variables are the contract and stay asserted); the node-id query count assertion is gone with the query.media_is_read_only_from_github_assets_…renamed…_from_github_hosts_…and rewritten for the host-owned classification (step 3: the contract moved toclassify; the old "private-user-images is not read" expectation was the old policy).a_collapsed_file_keeps_its_header_and_an_unavailable_one_its_placeholder(diff/list.rs) deleted (step 2: it asserted list item construction, not what the view draws; the collapse/placeholder layout is not otherwise covered by a test — it is checked visually).resolves_file_headers_independently_for_unified_and_split_listsmoved fromdiff/view.rstodiff/list.rswith the functions it covers (step 3, assertions unchanged).thirdparty/README.md:691–697with its hunk, gap expansion from text at the head. feat: add first-class support for CJK excalidraw/excalidraw#8530 — whole diff (288 files, binary woff2 fonts, renames, deletions), PR-body screenshots through the media read.tcode-headless serve(throwaway profile, same two links,--traverse off, direct QUIC on loopback) opened #120725 Conversation and Files and #8530 Conversation. PR replies on the wire, NDJSON line → deflated bytes on the connection's stream: #120725 conversation 10,909 → 3,388 B; files page 1 862,455 → 158,653 B; viewed files 25,703 → 2,164 B (its 15 s re-read on the same connection: 356 B); avatars 11,313 → 8,504 B and 2,953 → 2,000 B. #8530 conversation 109,337 → 11,644 B; avatars 2,237 → 1,536 B and 1,212 → 777 B; body images 279,796 → 83,034 B and 839,884 → 631,108 B. The NDJSON line sizes equal the desktop figures below; only the iroh stream deflates them. The phone figures sit below the desktop's 648 MB because the phone did not open #8530's Files (the 288-file whole diff the desktop lays out in full). Phone peak RSS (/usr/bin/time -l, debug build): 315.5 MB maximum resident (265.1 MB peak footprint) with #120725 Conversation + Files; 363.7 MB maximum resident (335.7 MB peak footprint) in one process holding #8530 Conversation (both decoded screenshots) and #120725 Conversation + Files page 1. Headless host 157 MB RSS.phone --localwith #120725 Conversation + Files: 298.5 MB maximum resident.ps): 253 MB at launch → 378 MB with #120725 conversation + 100 files rendered; → 648 MB with #8530 conversation (two large decoded screenshots) + 288 files. The 648 MB includes the eager list build: the shared diff renderer builds the rows of every loaded file up front (highlighted, both unified and split), not only the visible ones, so a whole diff of 288 files is laid out in full.Merge Danger
Door: two-way
Blast Radius: diff
Adds unreleased wire (Query::PullRequest incl.
PullRequestMedia::Unsupported, RefreshPullRequest, SetPullRequestFilesViewed; notes above PROTOCOL_VERSION). The Diff tab now renders through the extracteddiff/list.rs; its behaviour should be unchanged, but it was only checked by its tests here (the desktop Diff body is empty on main too — the separate bug Q8). Viewed marks write to GitHub as the host's account. Images in a pull request from non-GitHub hosts now cost one host round trip before the client loads them by URL.