Conversation
…ader The consumer half of `display-list-v2-window` (`protocol/proposals/display-list-v2-window.md` r2, producer PR #294, which this stacks on and which itself stacks #206 and #232 — for the integrator, not merged here). The producer's `apps/mac` co-signer row gates the capability on exactly one thing: the pane must paint an elided page as a placeholder before the shell may send the name, because today it would paint nothing there and a blank page is indistinguishable from a page that is genuinely empty — the confusion `PageContent::Elided` exists to prevent on the producer side and which the consumer must not reintroduce. This commit does the painting. The name is not sent yet; that is the last commit of this branch. ## The wire shape, fail-closed `RenderingV2.Page` gains `PageContent`, mirroring the producer's enum so that "this page was not built" is not representable as "this page is empty": * `items` present → resident (today's object exactly); * `items` absent with `"resident": false` → elided; * `items` absent with no flag → a **decode error**. That last rule is the whole reason §4 makes `items` absent rather than `[]`: a consumer that never learned about windows gets a refusal instead of a blank page. Both readers enforce it — `Codable` and the fast byte reader — and `DisplayList` gains `window`. A resident page still encodes with no `resident` key and an unwindowed list emits no `window`, so every line on the wire today is byte-for-byte unchanged, and so is every page digest built from one. `validate` gains the residency/window agreement check — an elided page with no window, a hole inside the window, a resident page outside it, a `document_page_count` that disagrees with the entries are all refusals — and skips item checks on elided pages while still requiring their page number and a positive frame. The `used ⊆ declared` direction of the feature check is already what §3's whole-document `required_features` needs; a test pins that, because the opposite direction would refuse a window whose only rule sits on a page the producer was not asked to build. ## The paint An elided page keeps its real frame, so the scroll column and page navigation land exactly where they would have — only the glyph-level content is windowed. `PageV2View` draws it as a dashed, tinted placeholder labelled "Page N / not loaded yet": not a white rectangle, not an error. It is not rasterized, has no hover and no tap target (there is no provenance on it to navigate to), and carries its own accessibility label. The pane header says "Showing pages a–b of N · k pages not loaded". ## The window follows the viewport, with hysteresis `PreviewV2Window` is a pure policy so both halves are testable without a window server. `PreviewAnchorProbe` reports the page range under the viewport, and only when it changes — a scroll inside one page reports nothing at all. `PreviewV2Window.next` then re-requests only when the reader comes within two pages of an edge of what is resident, or leaves it, centring `RenderingV2.maxWindowPages` pages on the viewport; the send is debounced with the compile debounce, so a flick through many pages is one request. The *echoed* window is always the input, never the requested one, so a window the producer narrowed to fit the reply limit (§8) does not loop. A document that fits in one window is never windowed at all, which keeps the common case — and `-delta`, which §7 makes exclusive with `-window` — exactly as it is today. And the case the proposal exists for: a document too large to serialise comes back `status: failed` with no pages and no sibling, so the shell cannot learn its size from a reply that does not exist. It does not try to — it retries once with a window at wherever the reader is, and the echo then says how big the document really is. ## What refuses while pages are elided `Export PDF (v2)`, `Export Exact PDF` and the v1 `Export PDF` all refuse a windowed frame and say how many pages are missing (§4.1: a windowed reply is never a PDF's source; §5.6: `v1::fallback` skips elided pages, so the v1 payload is a subset of the document). A windowed frame is never installed as a `display-list-v2-delta` base. Caret follow walks resident pages only, and ⌘⇧J reports that caret sync is limited to the loaded window rather than "inside no preview item" — a different claim, and the honest one. Implementation-Agent: claude-opus-5 (mac lane, linux-primary NixOS) Commit-Executor: claude-opus-5 (direct git; Cursor usage limit in effect) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xd5Hmwh5GHNTiAmHUJ1MZu
… reply through unaltered The three Rust crates on (or modelling) the route between the worker and the Mac shell, for `display-list-v2-window`. An audit of the route found the sibling *body* already opaque in both forwarding lanes — `document-runtime`'s `raw_display` retains the original text verbatim and its `display_candidate` validator reads only the envelope correlation fields; `preview-controller` forwards it as a `RawValue` (byte-exact) or as a `Value` (keys reordered, content preserved). Nothing there needed changing. What did need changing was everything that reads the *shape*: **`document-runtime` — the hard blocker.** `validate_reply_value`'s reply capability allowlist did not contain `display-list-v2-window`, so the moment a worker echoed the name the reply was rejected, which reaches `Session::fail`, which takes the process — the compiler session would be killed by the very capability meant to rescue it. The name is added, with the same requires-`display-list-v2` guard the images capability already has. The request payload gains `display_list_window` (skipped when absent, so the frozen runtime-v1 request bytes are unchanged) with a session setter mirroring `set_project_root`. Its v1 `pages` validator also required page numbers contiguous from 1. Per §5.6 `v1::fallback` *skips* an elided page rather than sending it as an empty one, so a windowed reply's v1 pages are a strictly increasing subset that starts at the window's first page. The contiguity rule is relaxed to "strictly increasing" only when the reply echoed `display-list-v2-window`; without it the original rule is untouched. **`preview-controller`.** `restart()` stripped `display-list-v2` from the capability set but left `display-list-v2-images` behind — and would have left `display-list-v2-window` — so a later compile asked for an extension whose base capability a previous restart had silently dropped. All three now go together. **`rendering-core`** is the reference model rather than a link in the route (nothing in tree depends on it), but it is what the Swift consumer mirrors, and `deny_unknown_fields` on `Page` and `DisplayList` made it refuse a windowed list twice over: unknown field `window`, missing field `items`. `Page` now hand-rolls its (de)serialization around a private wire shape, with exactly the Swift side's rule — `items` present is resident, `resident: false` with no `items` is elided, anything else is a refusal — and re-encodes a resident page with no `resident` key, so a resident page round-trips byte-identically and an unwindowed list emits no `window`. Validation skips item checks on elided pages and gains the residency-agrees-with-the-window rule. `bind_list`'s font binding already accepts a frame whose `fonts[]` is the whole-document closure while only the window's pages are resident — the rendering-core co-signer row's other half — and now says so. Tests: a windowed list round-trips without losing `window` or `resident`; an unwindowed one is byte-identical to before; a page with neither `items` nor `resident: false` is refused; residency disagreeing with the window is refused; `document-runtime` accepts the echo only when it was requested alongside `display-list-v2`, and a windowed v1 `pages` subset validates. Implementation-Agent: claude-opus-5 (mac lane, linux-primary NixOS) Commit-Executor: claude-opus-5 (direct git; Cursor usage limit in effect) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xd5Hmwh5GHNTiAmHUJ1MZu
…nted `setLiveV2` adds `display-list-v2-window` next to `display-list-v2` and `display-list-v2-images`. This is the last commit of the branch on purpose: the producer's co-signer row says the shell must not send the name until the pane paints an elided page as a placeholder, and until the commit before this one it did not. The name on its own windows nothing. §4: a reply is windowed only when the request *also* carries `display_list_window`, and this shell sends that in exactly two situations — a document with more pages than one window holds, and a compile that came back `status: failed` because the whole document would not fit (§1.0, the 500 KB case that has no preview at all today). Every other document gets the complete reply it gets now, including its `-delta` base. `FLASHTEX_DISPLAY_LIST_WINDOW=0` turns the request off for a session. `V2ImageTests` learns the third opt-in; `apps/mac/README.md` documents the capability, the placeholder, the viewport policy and what refuses while pages are elided. Implementation-Agent: claude-opus-5 (mac lane, linux-primary NixOS) Commit-Executor: claude-opus-5 (direct git; Cursor usage limit in effect) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xd5Hmwh5GHNTiAmHUJ1MZu
…bin/python3 The restart-capability test added in this branch was written against the old `tests/lifecycle.rs` convention; #307 replaced that with a resolver (`FLASHTEX_TEST_PYTHON`, else `/usr/bin/python3` when it exists, else the first `python3` on `PATH`) because NixOS has no `/usr/bin/python3`. The unit-test module cannot borrow the integration test's helper — it needs the private `layout_capabilities` field — so it carries the same resolver. `cargo test -p flashtex-preview-controller` now passes 89 tests, 0 failures, matching a control run of the same base. Implementation-Agent: claude-opus-5 (mac lane, linux-primary NixOS) Commit-Executor: claude-opus-5 (direct git; Cursor usage limit in effect) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xd5Hmwh5GHNTiAmHUJ1MZu
|
daniel-parent overlap heads-up: #367 (display-list-v2-window, draft on #294) and #333 (clickable links, the Mac consumer of |
… gaps (ordering only, on #476) (#411) * wip(#76): failing tests for Problems list bucket order Add DiagnosticsPanelTests that pin errors-then-warnings-then-gaps across two documents, Copy Diagnostics walking that order, and ⌘⌥] starting on the first author error rather than an earlier gap. What was run: `swift test --filter` of the three new methods in apps/mac. Result: Executed 3 tests, with 7 failures (0 unexpected) in 0.238 seconds. Current groups(of:) still sorts by first occurrence in source, so the tikz gap leads the list. Next: sort groups(of:) by bucket while keeping document order inside each bucket and inside each group's occurrences; add summary() for the header counts; wire ProblemsPanel VoiceOver. Open questions: whether ProblemsPanel.idealHeight 260 (~1/3 of a typical window) still needs a smaller default; overlap with #363/#367/#403. Implementation-Agent: cursor-agent cursor-grok-4.6 Commit-Executor: cursor-agent Lane-Owner: daniel-parent (mac-m5pro-dq222) Co-authored-by: Cursor <cursoragent@cursor.com> * wip(#76): sort Problems groups by bucket and show header counts groups(of:) lists author errors, then warnings, then FlashTeX gaps, keeping document order inside each bucket and inside each group's occurrences. result.diagnostics is not reordered. summary() reuses counts() for "2 errors · 5 warnings · 46 FlashTeX gaps"; the Problems header and VoiceOver label read that string. Opening height is 180 pt (was 260, ~1/3 of a typical window); the 40% cap is unchanged. What was run: swift test --filter DiagnosticsPanelTests| EditorDiagnostics*|WorkspaceShellTests in apps/mac. Result: Executed 54 tests, with 7 tests skipped and 0 failures (0 unexpected) in 2.107 seconds. Next: full `swift test` from apps/mac; overlap check; push. Open questions: none on ordering/header. Existing AppStorage values of 260 remain until the user resizes. Implementation-Agent: cursor-agent cursor-grok-4.6 Commit-Executor: cursor-agent Lane-Owner: daniel-parent (mac-m5pro-dq222) Co-authored-by: Cursor <cursoragent@cursor.com> * wip(#76): restore coloured Problems header count chips Restore the three SF Symbol count labels (errors red, warnings orange, gaps puzzle-piece secondary) including zeros so an all-gap document still shows the error column. VoiceOver uses summary() once via .ignore on the chips; the panel stays .contain so the list remains accessible. DiagnosticsListView caption matches. idealHeight stays 180; a stored AppStorage 260 cannot be told from a user resize, so it is left as-is. What was run: swift test --filter 'DiagnosticsPanelTests|EditorDiagnostics|WorkspaceShellTests' in apps/mac. Result: Executed 59 tests, with 7 tests skipped and 0 failures (0 unexpected) in 2.112 seconds. Next: daniel-parent review / PR. Open questions: none on the header. AppStorage 260 left as-is. Implementation-Agent: cursor-agent cursor-grok-4.6 Commit-Executor: cursor-agent Lane-Owner: daniel-parent (mac-m5pro-dq222) Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
|
Superseded — the whole page-windowing feature (producer + consumer) shipped under different PR numbers, via #665 ( Verified on Closed by the Commander after a stale-draft screening pass that required evidence in both directions — a landing SHA to close, an empty grep to keep open. If this verdict is wrong, say so on issue #2 and I will reopen: an asymmetric bar has produced wrong calls here in both directions before. |
The consumer half of
display-list-v2-window, and the second half of the fix fora 500 KB document that has no preview at all today.
What the producer asked for, and what this does
#294's §9 co-signer table has a row for
apps/mac, and it is a gate, not asuggestion:
So the branch is ordered that way, and you can check the ordering by commit:
d3ba3f05cdc38416rendering-core/document-runtime/preview-controllerpass a windowed reply through unalteredccfa49d3setLiveV2sends the name014fc2e8python3resolverWhat an elided page looks like
Not blank, and not an error. It keeps its real frame —
width/heightare onthe wire for exactly this — so it occupies precisely the room the built page
would, and scrolling and page navigation land where they always would. Over that
frame the pane draws a tinted fill with a dashed border, a document glyph,
"Page 41", and "not loaded yet", plus a corner label
page 41 · v2 · outside window. It is never rasterized, has no hover and no taptarget (there is no provenance on it to navigate to), and it carries its own
VoiceOver label: "Page 41, not loaded yet. Scroll here to load it." The pane
header says
Showing pages 33–48 of 385 · 369 pages not loaded · caret sync and PDF export are off for them.The fail-closed part is on the wire, not in the drawing.
RenderingV2.PagegainsPageContent, mirroring the producer's enum, and both readers enforce §4'sasymmetry:
itemspresent → resident (today's object, unchanged);itemsabsent with"resident": false→ elided;itemsabsent without the flag → decode error.That third rule is the whole reason §4 says "absent, not
[]". A resident pagestill encodes with no
residentkey and an unwindowed list emits nowindow, soevery line on the wire today is byte-for-byte what it is now, and so is every
page digest built from one.
How the window follows the viewport
PreviewV2Windowis a pure policy, so both halves are testable without a windowserver;
PreviewAnchorProbeis the only thing that touches AppKit.only when it changes — a scroll within one page reports nothing at all.
nextre-requests only when the reader is within 2 pages ofan edge of what is resident, or has left it. Inside that band the answer is
niland no request is sent. The new window ismaxWindowPages(24) centredon the viewport, clamped to the document.
is one request.
window, never what wasasked for, so a window the producer narrowed to fit the reply limit (§8) is
left alone as long as it still covers the viewport.
Two deliberate non-behaviours. A document that fits in one window is never
windowed at all — the common case keeps today's complete reply and keeps its
-deltabase, which matters because §7 makes-windowand-deltaexclusive.And the case the proposal exists for: a document too large to serialise comes
back
status: failedwith no pages and no sibling, so the shell cannot learnits size from a reply that does not exist. It does not try to — it retries
once with a window at wherever the reader is (page 1 until the pane has
reported a viewport), and the echo then says how big the document really is.
What is disabled while pages are elided
Per §4.1, a windowed reply is not a complete compile:
Export PDF (v2)andExport Exact PDFrefuse, and the toolbar buttonis disabled with a reason. The v1
Export PDFrefuses too — §5.6 hasv1::fallbackskip an elided page rather than send it empty, so the v1pagesof a windowed reply are a subset of the document. Each refusal namesthe damage: "this preview is a 16-page window over a 385-page document
(369 pages not loaded)". An export that silently dropped 369 pages would be
far worse than one that refuses.
-deltabase (§7). The request never carriesboth names, and the frame guards it independently.
CaretFollowwalks resident pages only —explicitly, not by accident — so the pane never scrolls to a placeholder as if
the caret were on it. ⌘⇧J reports "Caret sync is limited to the loaded page
window (pages 33–48 of 385); the caret maps to no page in it. Scroll to the
page you want and it is requested." rather than "inside no preview item",
which is a different claim.
The Rust route
An audit of everything between the worker and the shell. The sibling body was
already opaque in both forwarding lanes and needed no change:
document-runtime'sraw_displayretains the original text verbatim (its own test assertsc.raw().get() == input),display_candidatereads only the envelopecorrelation fields, and
preview-controllerforwards it as aRawValue(byte-exact) or a
Value(keys reordered, content preserved). What neededchanging was everything that reads the shape:
document-runtime— the hard blocker.validate_reply_value's replyallowlist did not contain
display-list-v2-window, so the moment a workerechoed the name the reply was rejected →
Session::fail→ the process istaken. The compiler session would have been killed by the capability meant to
rescue it. The name is added with the same requires-
display-list-v2guard theimages capability already has; the request payload gains
display_list_window(skipped when absent, so the frozen runtime-v1 request bytes are unchanged);
and the v1
pagescontiguity rule is relaxed to "strictly increasing" onlywhen the reply echoed the capability, because §5.6's subset does not start at
page 1.
preview-controller.restart()strippeddisplay-list-v2but leftdisplay-list-v2-imagesbehind — and would have left-window— so a latercompile asked for an extension whose base a previous restart had dropped. All
three go together now.
rendering-coreis the reference model rather than a link in the route(nothing in tree depends on it), but it is what the Swift consumer mirrors, and
deny_unknown_fieldsmade it refuse a windowed list twice: unknown fieldwindow, missing fielditems.Pagenow hand-rolls its (de)serializationwith exactly the Swift rule, re-encodes a resident page with no
residentkey,and validates residency against the window.
bind_listalready accepted aframe whose
fonts[]is the whole-document closure while only the window'spages are resident — the other half of that crate's co-signer row — and now
says so.
Verification
Mac (Xcode 26.6,
~/mac-build/window, clean.buildfor both runs):swift buildb6752e78(this branch's base)ccfa49d3Same failing set (empty), +22 tests.
014fc2e8is later and touches only a Rusttest module, no Swift. Note for anyone reproducing: the
Pagelayout changemeans an incremental
.buildfrom the base segfaults on unrelated tests —rm -rf .buildafter checking out.Rust, each against a control run of the same base in a separate worktree:
rendering-coredocument-runtime/usr/bin/python3, which NixOS lacks — #307 fixed this forpreview-controlleronly)preview-controllerThe 22 new Swift tests (
V2PageWindowTests) are the co-signer row, in its ownorder: the wire shape on both readers and through a round trip; the fail-closed
refusal of a page with neither
itemsnorresident: false; residencydisagreeing with the echoed window; §3's whole-document
required_featuresbeing accepted on a windowed reply; the elided page prepared at its real frame
and not rasterized; an empty page and an elided page being distinguishable;
the viewport policy (centring, clamping, the comfort band, a narrowed window not
looping, a small document never being windowed, the failure retry firing once);
an end-to-end scroll through a real
NSScrollViewin an off-screen window,asserting that a scroll inside one page reports nothing and a scroll to page 20
reports a range containing it; and the export/delta/caret refusals.
Open
The capability is advertised as of
ccfa49d3, which is what #294's gateallows now that the placeholder exists. It still windows nothing on its own: §4
requires the request to also carry
display_list_window, and this shell sendsthat only for a document with more pages than one window holds, or after a
compile that failed outright.
FLASHTEX_DISPLAY_LIST_WINDOW=0turns it off.Not done, and not claimed: the searchable-text cache and the accessibility
document model still build from whatever pages are present, so "Find in preview"
over a windowed frame searches the resident pages only. That is a narrower
version of the same whole-document problem and wants its own change.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Xd5Hmwh5GHNTiAmHUJ1MZu