Reader View — Design Spec
Milestone: reader-view (GitHub #313) Manual: TBD — the user manual needs a “Reading a document” section: the Page/Knowledge/Notes tabs, what each Knowledge sub-mode (Statements/Entities/Claims/Graph/Timeline/Map/Related) shows, and that clicking a statement reveals its source passage without losing your place.
Design-led spec (Testing Constitution). The creative director owns the intent; tests enforce it; code makes them pass. One line per behavior, each cited by its pinning test. Status: DRAFT — PASS 1 of 2 (built behaviors only, grounded in disk 2026-09-18; the fold-in of the legacy “Reader View - Page” milestone’s issues, and the 14 issues from the
library-view-modes.mdledger waiting on this spec, is PASS 2 — a fold plan follows this spec’s own behaviors, no issues moved yet). Tags: [OK] behaves this way today · [PARTIAL] implemented, not fully verified/tested · [BROKEN] regression, code contradicts the line · [GAP] intended, never built.
Intent (the design)
The Reader is one of the three surfaces the standing rule protects from ever merging into each other: Preview, Reader, and Inspector never merge — Preview is the quick look, the Reader is the WebKit-hosted, page-by-page reading surface, and the Inspector is curation and metadata. This spec covers the Reader’s OWN internal structure only: what tabs it has, what each tab shows, and how a click on one surface reaches another without losing your place.
The Reader collapses what used to be ~10 separate WebKit modes into three top-level tabs —
Page (read the source), Knowledge (explore what we know), Notes (the human
reading layer) — with the Knowledge tab further split into native sub-modes (Statements,
Entities, Claims, Graph, Timeline, Map, Related). A single lens enum
(ReaderLens) is the one user-facing inventory of every surface that actually exists; the
two-enum split beneath it (ReaderTab for the top-level tab, KGSurfaceTab for the
Knowledge sub-mode) is internal vocabulary the lens maps onto — one user-facing list, two
internal switches, no third source of truth. This spec does not cover the Library’s own view
modes (library-view-modes.md) or the Preview pane; a lens/tab existing in the enum is a
promise that its surface renders, not a description of the Library or Preview.
Ratified rulings honored here, not restated: modes-to-panes.md’s
m2p.reader-consults-the-plan — the Reader pane renders whatever
PaneContentPlan.plan(for: viewMode).reader decides, never routing off a resolved Document
alone; and that same spec’s “renditions per surface” matrix (§”Selection (AppViewMode) |
Library | Source/Preview | Reader | Inspector”), which is the authoritative answer for what
each SELECTION KIND shows in the Reader — a workflow’s run log, a chat’s document, an
entity/claim row’s readable paragraph (still GAP there, see below), and so on. This spec is
downstream of that matrix for document selections specifically; it does not re-litigate which
selections reach the Reader at all.
kg-readable-representation.md’s kg.read.lives-in-reader ruling — the entity biography
paragraph is DESIGNED to live in the Reader, not the Inspector — is cited, not restated: it
remains [GAP] there today (the paragraph still renders in the Inspector,
DocumentInspector.swift:207). This spec’s own Statements lens is the Reader-side surface
that ruling will eventually redirect into; see section D below for the plain statement of the
Statements/Claims overlap the maintainer asked about.
The layers drawn on a page and on each line in the lines mode (#5414) — translation, names,
statements, notes — are specified once, for every page surface, in layers-on-the-source.md.
Surfaces: ReaderLens (Views/Reader/Surfaces/ReaderLens.swift), ReaderTab
(Views/Reader/Surfaces/ReaderTab.swift), KGSurfaceTab
(Views/Reader/Knowledge/DocumentKGSurface.swift), DocumentKGWebPane (the WebKit host for
Transcript/Statements), DocumentKGSurface (the native-vs-WebKit dispatcher for the
Knowledge tab), PaneContentPlan.ReaderPageRoute/ReaderSubject
(Views/Shell/PaneContentPlan.swift), ContentView+SourceNavigation.swift
(sourceRevealDocument), ReaderPassageFocus (Views/Reader/Page/ReaderPassageAnchor.swift).
Behaviors (each → one pinning test)
A. The lens is the one inventory; two internal switches, no third source of truth
reader.lens.every-lens-maps-to-a-surface-that-exists— [OK] everyReaderLenscase maps to aReaderTab(via.tab) and, for the Knowledge lenses, aKGSurfaceTab(via.representation) — a lens existing is a promise the surface renders, not aspirational naming. Verified at HEAD:ReaderLens.swifthas exactly nine cases (page, statements, entities, claims, graph, timeline, map, related, notes); the deliberate absence of a “Translation” lens is stated in the enum’s own doc comment (“a lens that goes nowhere is the menu lying”). Pinned (filefichero/Tests/Unit/general/Views/Reader/ ReaderLensInventoryTests.swift, suiteReaderLensInventoryTests):ReaderLensInventoryTests.lensesAreAnInventory,ReaderLensInventoryTests.noLensGoesNowhere.reader.lens.round-trips-tab-and-representation— [OK]ReaderLens.lens(for: representation:)is the exact inverse of.tab/.representation— restoring saved state or a menu-bar selection lands on the same row the pane head shows, and every knowledge lens names a DISTINCTKGSurfaceTab(no two lenses silently collapse onto one sub-mode). An unknown sub-mode falls back to.entitiesrather than crashing. Pinned (same file, suiteReaderLensInventoryTests):ReaderLensInventoryTests.lensesDoNotCollide,ReaderLensInventoryTests.lensRoundTrips,ReaderLensInventoryTests.unknownRepresentationFallsBack.reader.head.is-the-one-lens-selector— [OK] the Reader’s own top-tab bar was retired (R3, 2026-08-23) in favor of the pane head’s lens selector — the head and the menu bar read ONE published lens, not two independent pickers that could disagree. Pinned (same file, suitePaneHeadWiringGuardTests):PaneHeadWiringGuardTests.tabBarRetired,PaneHeadWiringGuardTests.oneBindingRenderedTwice.
B. Every Knowledge sub-mode is mounted, not a stub
reader.knowledge.every-sub-mode-is-mounted— [PARTIAL] (→ #4867)DocumentKGSurface’s dispatcher mounts a REAL view for all eightKGSurfaceTabcases, none a placeholder: Transcript and Statements (.digest) render inside the sharedDocumentKGWebPaneWKWebView (kept alive across tab switches via a ZStack + opacity swap, so scroll position survives); Graph mountsForceDirectedGraphView; Timeline mountsKGTimelineView; Map mountsKGMapView; Claims mountsKnowledgeGraphInspectorSection; Entities mountsDocumentInspectorEntitiesTab(the SAME component the Inspector’s own Entities tab uses); Related mountsDocumentInspectorRelatedTab(ditto, “a second implementation would be a second answer to what this is related to”). Verified at HEAD:DocumentKGSurface.swift’scontent/nativeTabContent(:294-462) — every case in theswitch activeTabhas a real construction, notEmptyView()(the onlyEmptyView()cases are.transcript/.digest, which correctly go to the WebKit pane instead, not because they’re unbuilt). No dedicated render test asserts every case mounts without crashing; grounded in reading the exhaustive switch directly (a newKGSurfaceTabcase fails to compile here until decided, the same disciplinePaneContentPlan.Cell.readerPageRouteuses).reader.knowledge.map-and-timeline-scope-to-this-document— [OK] the Reader’s Map and Timeline sub-modes are scoped to the document being read (sourceDocumentId: documentIdpassed to bothKGTimelineViewandKGMapView), not the whole library’s first 500 claims — a document-scoped reading surface showing library-wide data because the scope parameter was missing was the actual defect this fixes. Pinned:MapLensScopeTests.testTheReaderScopesBothSurfacesToItsDocument(filefichero/Tests/Unit/general/Views/Library/MapLensScopeTests.swift, suiteMapLensScopeTests).
C. Passage-level source reveal (landed tonight, e71bb070b, #4834/#4852)
reader.reveal.transient-state-preferred-over-shown-document— [BROKEN] (→ #4834, retagged 2026-09-19 from the maintainer’s own test session — was [OK]) clicking a sentence in the entity’s biography does NOT make Reader and Preview follow; Preview shows “No selection” throughout. The lesson, stated plainly:ClaimSourceLandingTests(cited below) passed and this behavior still failed on screen, because those tests are source scans and pure functions — none of them mounted a pane. A green pinning test proved the mechanism’s shape; it never proved a real pane reads it. Cause, VERIFIED on disk (2026-09-19): the reveal’s own call tofocusClaimomits the entity argument, sofocusClaimassigns its default —nil— unconditionally, clearing the focused entity; a regression of e71bb070b (Slice A), fixed in 3f017efac, build passes, but NOT yet seen working (tests haven’t executed). Tag stays BROKEN until the maintainer sees it work. Seekg-readable-representation.md’skg.read.sentence-opens-source-highlighted. What follows (verified at HEAD:sourceRevealDocumentis set, read by Preview and the Reader…) describes the mechanism as designed and unit-tested — the claim under test, now known false in the running app: a click on a knowledge surface (a statement, a claim) reveals its source in the Reader and the source image WITHOUT changing the current selection or switching sidebar mode — Preview and the Reader prefer a transientsourceRevealDocumentover whatever document is otherwise “shown,” while the Inspector never reads it and keeps the entity it was already showing. Verified at HEAD:sourceRevealDocument(Views/Shell/ContentView/ContentView+SourceNavigation.swift:129-190) is set byfocusKGSourcePreview/the resolved-location path, read by Preview and the Reader, and explicitly never read when computinginspectorDocument. It clears on any real selection change or focus change (guarded against a same-frame resolve wiping its own write) and never reaches the workflow-run selection. Pinned:ClaimSourceLandingTests(filefichero/Tests/Unit/general/Views/Library/ ClaimSourceLandingTests.swift, suiteClaimSourceLandingTests; cases “a knowledge-surface reveal never writes sidebar mode or selection,” “inspectorDocument never reads sourceRevealDocument,” “a real sidebar or browser selection change clears the reveal,” “a knowledge-surface reveal never reaches the workflow run selection”).reader.reveal.destination-is-explicit-not-a-silent-default— [BROKEN] (→ #4834, retagged 2026-09-19 from the maintainer’s own test session — was [OK]) clicking a claim in the Inspector goes to the page, the highlight is not precise, and he loses his place — the “reveal without losing your place” property this behavior and its sibling above both claim does not hold on screen. The lesson, stated plainly:ClaimSourceLandingTests(cited below) passed and this behavior still failed live, because those tests are pure-function/ contract tests — none mounted a pane. Cause, VERIFIED on disk (2026-09-19), the same root cause as the sibling above:focusClaimis called without the entity argument and clears the focused entity, a regression of e71bb070b (Slice A); fixed in 3f017efac, build passes, but NOT yet seen working (tests haven’t executed). Tag stays BROKEN until the maintainer sees it work. What follows (about 25 construction sites audited…) describes the mechanism as designed and unit-tested, now known false in the running app: a source request now STATES its destination (.reader, or both panes) rather than falling back to a silent.readerdefault — about 25 construction sites were audited; knowledge surfaces that are wired ask for both panes, navigational surfaces (source outline, annotations, the artifacts inspector) keep opening the Reader deliberately, and surfaces not yet wired state.readerexplicitly so their behavior is provably unchanged, not accidentally inherited. Pinned:ClaimSourceLandingTests("a .reader request still navigates through the selection-changing path","both highlight channels are posted from the one resolved request").reader.passage-channel.has-a-second-producer— [PARTIAL] (e71bb070b, → #4834 — tracked on its own spec,kg-readable-representation.md) the Reader’s passage-highlight channel,ReaderPassageFocus— previously written only by in-page SEARCH matches — now has a second producer: following a claim’s source latches the same passage anchor before navigating, reusing the identical match-then-clear mechanism a prior fix (787b624c2) built for search, rather than inventing a second highlight path. Verified mounted at HEAD:ReaderPassageFocusis read byReadingPaneViewandPageContentPane+SourceHighlight.swift(the actual highlight-drawing code), and written by both the search path andContentView+ClaimPassageLanding.swift(the new claim-following path). Pinned:ClaimSourceLandingTests("a claim with a quote latches a passage anchor before navigating","the latch is consumed by the document that used it, not by another","following a claim source records and posts a passage anchor"). PARTIAL rather than OK: the maintainer’s own confirmation that both highlights (passage + source-image region) render live together is still open, perkg-readable-representation.md’skg.read.span-reuses-existing-location-resolver, which this behavior is the Reader-side half of — cited there, not duplicated as a second open question here.
D. The Statements/Claims overlap, stated plainly (#4855)
The Reader’s Statements lens (KGSurfaceTab.digest) and Claims lens
(KGSurfaceTab.claims) render the SAME underlying KnowledgeClaim rows two different ways —
Statements as WebKit prose grouped by entity, Claims as a native, source-grouped list — and
today each lens’s tooltip describes itself using language that belongs to the OTHER lens.
This is kg-readable-representation.md’s own finding
(kg.read.statements-lens-becomes-the-paragraph, [BROKEN], #4855) and is cited here, not
re-diagnosed: that spec already verified the exact lines
(DocumentKGSurface.swift:101/:105) and named the direction — Statements should become the
readable paragraph the entry composer produces (kg.read.lives-in-reader’s eventual home),
while Claims stays the structured, editable table, so the two differ in KIND (prose versus
data) instead of overlapping in “statement” language. This spec’s own helpText values above
(DocumentKGSurface.swift:100-101) are the exact evidence that finding cites; nothing further
to add here beyond confirming, from this spec’s own reading of the same file, that the
overlap is real and unresolved.
PASS 2 — the fold (25 issues read fresh: the 14 waiting + #248’s 11)
Reading every body fresh surfaced a correction the titles alone didn’t show: several of the
14 “waiting on reader-view” issues turn out to be about the Preview pane, not the
Reader’s WebKit surface — PDFPageWithToolbar (Views/Preview/PDFViewer/), DocumentCanvas
(Views/Preview/), and the sibling-swipe image-viewer code (Views/Preview/ImageViewer/)
are all Preview components. They were mis-scoped as reader-view candidates by title
resemblance (“reading surface,” “document canvas”) to actual Reader vocabulary. Corrected
below rather than folded in on a title match.
E. New Page-tab behaviors (fits, cited from HEAD, moved onto #313)
reader.page.scope-selector— [GAP] (#1749, #1483 — the same ask filed twice) the WebKit/text reading view should offer a scope selector — current page / folder / parent / grandparent — reusing the existing at-page/at-folder scoping concept (an already-closed earlier feature) rather than inventing new scope levels. Not verified as built.reader.page.catalogue-pane-and-transcript-always-shows— [GAP] (#1567) a separate Catalogue/Content pane (left of the reading surface) showing the structured extraction output, distinct from the transcript stream; and the WebKit transcript should always render SOME transcript for the selection, including the aggregated transcripts of a parent’s children (a chapter PDF’s per-page transcripts), not “No transcript available” when children have them. The issue’s OWN separate ask — transcript click-to-source — is now substantially built:reader.reveal.transient-state-preferred-over-shown-documentandreader.passage-channel.has-a-second-producerabove are exactly that mechanism, landed since this 2026-06 issue was filed. Not verified as built for the catalogue-pane and parent-aggregation halves.reader.lens.translation-not-yet-built— [GAP] (#2093, #3544 — the same gap, #3544 additionally proposing Transcription+Translation as sub-tabs under Page once shared sub-tab support lands) a first-class translation lens — view a document/page WITH its translation side-by-side or toggled, at doc/page/passage granularity, persisted as a reusable artifact — does not exist.ReaderLens.swift’s own doc comment already states this precisely: “Translation is absent because it does not exist… a lens that goes nowhere is the menu lying.” This behavior gives that absence its own line with the issues that ask for it, rather than leaving it only as a code comment. Not built.reader.lens.book-view-not-yet-built— [GAP] (#1194) a full-document “Book” reading mode — the complete transcription typeset as continuous prose (not page-by-page), with claim passages and entity names highlighted inline and a claim popover on click — is a lens this spec’s inventory does not have. This is a NEW lens proposal, not an extension of an existing one (Page shows source page-by-page, Statements is per-entity SVO prose, Notes is markup) — a genuinely new reading mode. Not built.reader.page.selection-scope— [GAP] (#4597) with multiple items selected, the Reader should show the SELECTED items, not the whole folder — the same “selection scope, not ambient scope” principlelibrary.chrome.select-all-follows-the-visible-surfaceandlibrary.canvas.trackpad-scroll-pans’s neighbors already apply elsewhere in this codebase. Not verified as built.reader.page.word-level-source-and-find— [GAP] (#4405) two behaviors the issue argues are one feature (both are “given a position in the text, show me where that is on the page”): clicking a word or line reaches its source region (page-level ships first, honestly, before per-word bounding-box geometry exists; never approximating a location it can’t prove); and an in-reader Find with match count and next/previous, native-Find-equivalent. Builds directly onreader.reveal.transient-state-preferred-over-shown-documentandreader.passage-channel.has-a-second-producerabove — the “one cursor” constraint the issue states (a click, a search step, and a scroll all move the SAME focus) is exactly what those two behaviors already guarantee for the claim-following case; this issue extends the same mechanism to word-level clicks and to search. Not built.reader.page.inline-transcription-editing— [GAP] (#4375) editing a page’s transcription directly in the WebKit reader, in place, rather than requiring a trip to the inspector. The issue’s own scoping is the honest one: the mechanic (contenteditable+ the existing script bridge) is easy; the integrity work is real (offset remapping for dependent KG/bbox spans, the audited save path, concurrent-write watermarking, diplomatic-vs-normalized ambiguity). Not built.reader.page.highlight-layer-precedence— [GAP] (#4355) with several highlight kinds now live in the Reader (find-in-page matches, recognized-text bounding boxes, knowledge highlights, selection/annotations), which one is visually “in front” should be a function of the visible split/pane, not every layer drawing independently and turning the page to visual noise. Not built.reader.page.transcript-wraps-not-clips— [BROKEN] (#3805) the transcript should WRAP at a narrow pane width; it currently CLIPS, cutting off the left edge of every line. Verified BROKEN at HEAD:document_view.html’s.transcriptrule (fichero-server/src/fichero_server/api/templates/document_view.html:147-154) is a CSS grid with nogrid-template-columnsdeclared — a grid item’s defaultmin-width: autoresolves to min-content (the longest line) for awhite-space: pre-wrapblock, so the track refuses to shrink below the longest line and overflows instead of wrapping. The issue’s own proposed fix (grid-template-columns: minmax(0, 1fr)) is not present in the file today.reader.page.sepia-paper-themes— [GAP] (#3721) Sepia/Paper reading themes on top of the existing Match App/System choice, driving the same CSS custom properties the Reader already var-drives (systemThemeCSS()/themeInjectionScript()inDocumentKGWebPane.swift) — no new injection path needed, just new theme values. Not built.reader.surfaces-read-annotation-store-optionally— [PARTIAL] (#4898)ReadingPaneView/PageContentPanereadAnnotationStoreas an optional@Environmentvalue rather than force-unwrapping it, per house rule #4513 (degrade, never trap). Built:dbf5b983d. It was a defensive hardening fix, not a confirmed-reproduced one — the commit’s own message says it could not reproduce the crash trigger from a clean launch. PARTIAL because nothing tests either half: a PLAIN unit test CAN cover the nil-safety half today —pageAnnotations,loadAnnotations(), and the fouraddNotepaths can be extracted as functions takingAnnotationStore?directly and called withnil, asserting a graceful no-op/[]rather than a crash, no view mounting required. What a plain test CANNOT prove is that the app ever actually mountsReadingPaneView/PageContentPanewith a nil store in a real ancestor chain — that half still needs a mounted reproduction (an XCUITest from the exact restored-scene path that triggers it, if one is ever found), and a test that only covers the nil-safety half would keep passing even if the real trigger were never real to begin with.reader.one-renderer-handles-writing-direction— [GAP] (#4915, recorded from the source-model spec work, branchspec/page-model,specs/source/models-chains-and-projects.md— not in this tree) one code path renders a document’s text, and a document’s actual language/script drives both itslangand its writing direction. Verified: TWO separate Reader renderers exist today — the nativeViews/Reader/Page/DocumentTextReader.swiftand the engine’s HTML templateapi/templates/document_view.html, which hard-codes<html lang="en">(line 2) regardless of the document’s own language. Verified: neither file contains anydir=/RTL/writing-direction handling — zero hits by grep in either. Not built.
F. Redirected to an existing spec
- #2515 (reader toolbar overlaps the library/filmstrip; overflow doesn’t collapse to “…”
before it does) →
panes-workspaces.md, which already owns toolbar/mini-toolbar overflow chrome (see this spec’s own earlier fold of #125/#251). New behavior there:panes.toolbar.reader-overflow-collapses-before-overlapping[GAP]. - #2419 (PDF viewer needs the magnifier/loupe control at the bottom, matching the image
viewer) →
preview-magnifier.md, whose entire scope is the magnifier control. New behavior there:magnifier.pdf-viewer-has-the-loupe-too[GAP].
G. Verify-close — evidence posted, left OPEN, not closed here
- #4605 (“Restore the reader graph view: subject-verb-object statements laid out”) — the
complaint this issue describes (a graph surface that “used to exist” and became reachable
only through the menu bar) is verified FIXED by the R3 lens reform this spec’s section A
documents:
KGSurfaceTab.graph→ForceDirectedGraphViewis a liveReaderLenscase, reachable from the pane head, not menu-bar-only. NOT independently verified: whether the graph specifically renders “subject-verb-object statements… laid out” (an SVO-statement layout) versus the entity-co-occurrence networkForceDirectedGraphView’s own doc comments describe — those may be the same thing described two ways, or two different asks. Left OPEN for the maintainer to confirm which.
H. Waiting on a spec that doesn’t exist (left on the legacy milestone, not moved)
Reclassified from “waiting on reader-view” (already resolved by Pass 1) to “waiting on a Preview-surface spec” (still doesn’t exist) after reading the bodies fresh — all six are Preview-pane components, not Reader ones:
- #2257 (PDFPageMapView, a 2D page grid) —
PDFPageWithToolbarisViews/Preview/ PDFViewer/. - #2090 (multi-page 1-up/2-up/N-up layouts) — names
DocumentCanvas.swift/PDFPageView.swift, bothViews/Preview/. - #2040 (multi-select continuous-scroll preview) — its own title says “preview”; body: “the image preview becomes a continuous scroll.”
- #1817 (continuous-scroll multi-image viewer) — “the image viewer,”
Views/Preview/ ImageViewer/. - #1747 (unify page/image navigation, “document canvas”) — same
DocumentCanvas.swiftas #2090. - #1552 (swipe changes pages on PDF, not images, in a folder) —
PDFPageController, the image-viewer sibling-swipe gesture — bothViews/Preview/.
Also waiting on the same not-yet-existing spec, from #248: - #4587 (Preview zoom-out has no sensible floor at 1%) — Preview’s own zoom control.
I. Maintainer triage — no home found
- #1253 (bidirectional scroll sync, Reader’s WebKit transcript ↔ Preview’s native PDF
viewer) — genuinely cross-surface: the Reader half already has the mechanism this spec
documents (
ReaderPassageFocus), but syncing INTO Preview needs the Preview-surface spec that doesn’t exist yet, and “keep two different panes in scroll sync” isn’t obviously either surface’s spec to own alone. - #973 (book-aware page numbering + chapter markers via Apple Intelligence detection) —
fundamentally a backend detection + citation-label feature (
Document.metadata ['book_structure'], rendered inClaimSummaryCard/EntityKindRow), not a Reader-tab question; no spec read this pass owns citation-label formatting. - #1491 (TL-3: source outline endpoint, hierarchical drill-down API) — backend-only
(“Thinking Layer” program,
docs/contributor_manual/architecture/thinking-layer.md), no UI surface of its own to fold into.
Fold table (issue → spec → behavior id → tag)
| Issue | Spec | Behavior id | Tag |
|---|---|---|---|
| #1749, #1483 | reader-view | reader.page.scope-selector |
GAP |
| #1567 | reader-view | reader.page.catalogue-pane-and-transcript-always-shows |
GAP |
| #2093, #3544 | reader-view | reader.lens.translation-not-yet-built |
GAP |
| #1194 | reader-view | reader.lens.book-view-not-yet-built |
GAP |
| #4597 | reader-view | reader.page.selection-scope |
GAP |
| #4405 | reader-view | reader.page.word-level-source-and-find |
GAP |
| #4375 | reader-view | reader.page.inline-transcription-editing |
GAP |
| #4355 | reader-view | reader.page.highlight-layer-precedence |
GAP |
| #5466, #5473 | reader-view | reader.highlight.search-hits-all-lit |
OK |
| #3805 | reader-view | reader.page.transcript-wraps-not-clips |
BROKEN |
| #3721 | reader-view | reader.page.sepia-paper-themes |
GAP |
| #2515 | panes-workspaces | panes.toolbar.reader-overflow-collapses-before-overlapping |
GAP |
| #2419 | preview-magnifier | magnifier.pdf-viewer-has-the-loupe-too |
GAP |
Node-model/prototype-system EPIC family — for the maintainer, comparison only
Four open issues describe the same underlying “Tinderbox-style prototypes/classes” idea, filed across more than a year, none marked as superseding another:
| Issue | Filed | What it adds beyond the others |
|---|---|---|
| #1570 | oldest (lowest number) | The broadest framing: “Phase 1 workspace-items → Phase 2 general” — proposes a staged rollout starting from workspace items specifically, generalizing later. The only one of the four that proposes a PHASING plan rather than a single scope. |
| #1741 | mid | Framed as a revisit — “Class / prototype system — revisit after code cleanup” — the only one that explicitly makes the work CONDITIONAL on a prior cleanup landing first, rather than standalone. |
| #2081 | mid-late | The most FULLY SPECIFIED of the four: “prototypes (classes) + aliases + entities-as-nodes (Tinderbox-for-archives)” — names three concrete sub-features (prototypes, aliases, entities-as-nodes) rather than one phrase, and is the one most other issues in the fold cross-reference (#2114 names it directly as the hook it extends). |
| #2114 | newest | The narrowest and most ACTIONABLE scope: “Prototype definitions + attribute inheritance + resolve prototype_key (extend the existing hook)” — the only one of the four that names an EXISTING hook to extend rather than proposing new architecture, suggesting partial groundwork may already exist for this one specifically. |
No disposition proposed here — this is the comparison the maintainer asked for, not a recommendation for which one carries the work.
Triaged from the backlog (2026-10-04)
reader.page.selected-page-scope— [GAP] (#4559) selecting one page of a PDF shows that page in breadcrumb, preview and Reader, and the Reader lists only that page, not the whole document transcript.reader.content-always-present— [GAP] (#1445) Every page always shows a Content area (empty placeholder, never ‘No transcript’) and it refreshes live after Transcribe; same root cause as #5077.reader.webcontent-gpu-crash-free-switching— [GAP] (#1615) Switching documents or folders never kills the WebKit GPU process; recovery exists (06a32b50a) but the cause is unproven.reader.ios.page-fills-pane— [GAP] (#2497) on iPhone the reader page fits the available width with minimal margin, and double-tapping an icon in the library opens the preview (single tap selects).reader.keyboard.navigate-without-mouse— [GAP] (#3688) in the Reader, keys move between pages, jump by paragraph, move focus between entities/claims and trigger source-reveal, wrapping the WebKit content.reader.fonts-and-colors-settings— [GAP] (#3411) Settings offers Fonts & Colors for sidebar/library, reader, inspector editor and list preview (only editor.fontName/fontSize exist today in GeneralSettingsView.swift:18-19).reader.page-filmstrip— [GAP] (#2484) the full-screen reader shows a collapsible thumbnail strip along the bottom that scrubs to a page or image (ImmersiveReaderView+Controls.swift:74 still a TODO).reader.voiceover-paragraphs— [GAP] (#3692) VoiceOver reads the archive text paragraph by paragraph and a custom rotor navigates by paragraph.reader.voiceover-paragraph-to-note— [GAP] (#3693) an accessibility action captures the current paragraph into a Reader note.reader.select-all-selects-the-readers-text— [GAP] (#4376) with the Reader focused, ⌘A selects all of the Reader’s text; the Library half is built (library.chrome.select-all-follows-the-visible-surface) and the Reader half has no code or test.reader.scope.current-page-only-is-one-click— [GAP] (#5005) the Reader has a visible switch between the whole folder and just the selected page (the scope lens: selection, peers, parent, children).reader.order.reveal-line-in-preview[PARTIAL] (#5424): double-clicking a line in the Order tab or the Order pane, or clicking a line in the Reader, scrolls and zooms the linked Preview to that line and selects its box. One reveal action, shared by every surface. Tested through the real selection store. Ruled 2026-10-04: a single click in the Order list lights the box and the Inspector stays put; the Reader follows the caret, so moving onto a line by keys zooms too; a PDF page’s Preview zooms as an image’s does, from the same reveal rect. (WindowState.revealSegments→InspectorPath.reveal; the image Preview zooms withzoomToNormalizedRegion, the PDF Preview withPDFZoomController.zoom(toNormalized:).LineRevealTestsproves both surfaces give the same segment, box and zoom rect, that a reveal from the Inspector’s own Order tab leaves the Inspector where it was, and that the rect zooms a realPDFView. Not yet seen in the app: no real click is driven.)reader.page.header-icon-drags-the-page[GAP] (#5290): the page header’s one icon is a proxy icon: dragging it carries the page out, with the same payload as dragging the page from the Library. (The file name or ‘file.pdf › Page N’ title and the single icon landed in f75912435.)reader.lines.image-above-text[PARTIAL] (#5414): the Reader has a lines mode: down the page, each line’s crop of the source image sits above that line’s transcription, in reading order, and it follows the selection like the other Reader modes. (The Lines lens,ReaderLinesView: the working pass’s lines (ReaderLines.linesoverSegmentStore.workingSegments) in the engine’s order, each cut bySegmentPictureServiceabove the reading that counts; a selected line, or a selected word’s line, is lit and scrolled to, and a click on a picture reveals it in the Preview throughWindowState.revealSegments.fichero/Tests/Unit/general/Views/Reader/ReaderLinesTests.swiftproves the rows, their pictures and readings, and the selection’s lines over the recorded page. Left: the order is the engine’s index, not a named reading order; not yet seen in the app.)reader.lines.edit-saves-a-persons-reading[OK] (#5414): a line’s text is editable in place; a correction is saved as a person’s reading through the same action the Inspector’s reading editor uses. Tested through the real save action. (InspectorReadingEdit.save, the Inspector’s Edit… save, throughReaderTextEditRunner:fichero/Tests/Unit/general/Views/Reader/ReaderLinesTests.swiftshows the row’s request is the Inspector’s ownrepresentation.create, correcting and checked against the counting reading, and that unchanged words write nothing.)reader.lines.zoom-scales-together[PARTIAL] (#5414): zoom scales image and text together, and the line image is always drawn larger than its text so the hand can be read. (The Reader’s own zoom (webZoom, 50%-300%) drivesReaderLines.Metrics: the picture’s extent across the line is three times the words’ point size at every zoom, proven infichero/Tests/Unit/general/Views/Reader/ReaderLinesTests.swift; the drawn sizes are not measured on screen.)reader.lines.follow-direction[PARTIAL] (#5414): lines follow their direction: a vertical line’s image strip stands beside its text column (direction from the cascade, #5411). (ReaderLines.arrangementfromSegmentStore.direction(of:), the Inspector editor’s reading of it; a ttb/btt line stands beside a vertical editor, an rtl line reads above. Proven infichero/Tests/Unit/general/Views/Reader/ReaderLinesTests.swift; not yet seen in the app.)
What the Reader can highlight (read from the code 2026-10-04)
The Reader’s page is the engine’s served page (fichero-server/src/fichero_server/api/templates/document_view.html),
driven from the app through window.fichero. The colour a highlighted segment takes, and how a
region’s lines show as its children, are owned by source/segment-editor.md, “Box colour and the
segment hierarchy” (source.editor.hierarchy.reader-shows-regions, #5426).
| Highlight | State | Where |
|---|---|---|
| A line | built | the Source view’s selection is shown on the Reader’s lines: ReaderLineSelection.showLinesScript calls the page’s showLines, which lights each selected line’s span (selectedLineSpans, applyShownLines, a CSS highlight). Pinned by fichero-server/tests/unit/api/test_reader_selection.py and fichero/Tests/Unit/general/Views/ReaderLineSelectionTests.swift. |
| A word | missing | selectedLineSpans matches only line ids; a selected word’s id is skipped, so a word picked in Preview lights nothing in the Reader. |
| A region | missing | a region’s id is skipped the same way; its lines are not lit. |
| Search hits | built | a library search lights every hit on the page from its anchors, the current one stronger (ReaderSearchMatchState → pageHitsScript → __ficheroLightPageHits); find-in-page counts and steps through matches (ReaderFindInPage). Both draw through one rule, __ficheroLightHits (CSS highlights fichero-find/fichero-hit, same colours). Pinned by ReaderAllHitsTests and ReaderSearchPassageLandingTests. |
| Statements (SVO claims) | partial | highlightClaim → updateTranscriptHighlight marks the FIRST text occurrence of the claim’s source_excerpt, not the passage the claim is anchored to; a repeated phrase lights the wrong place. |
| Entities | partial | highlightEntity lights the excerpt of the entity’s first claim, not the places the name occurs on the page. |
reader.highlight.word-from-selection— [GAP] (#5155) a word selected in the Source view, the Segments list or the Inspector is lit in the Reader’s text at its span, as a selected line is.reader.highlight.region-from-selection— [GAP] (#5155) a region selected elsewhere lights every line of that region in the Reader’s text, and the Reader scrolls to its first line.reader.highlight.statement-on-its-anchor— [GAP] (#4932) a statement focused in the Reader lights the passage its anchor names (segment, reading and span), not the first match of its excerpt’s text.reader.highlight.entity-mentions— [GAP] (#1659) an entity focused in the Reader lights every place its name occurs on the page, from its anchored mentions (layers.entities.shown-on-text-and-image), not one claim’s excerpt.reader.highlight.search-hits-all-lit— [OK] (#5466, #5473) every search hit on a Reader page is lit at once, with the current one stronger: a library search’s anchored hits on the page and the find bar’s matches alike, through one rule (__ficheroLightHitsinReaderFindInPage.swift, one colour pair). Next/Previous in the find bar move the current match and leave the others lit; ending the search or dismissing the find bar unlights them all; an empty find does not unlight the search’s hits. Pinned in a realWKWebViewbyReaderAllHitsTests. The engine’s search reports every match on the page as an anchored excerpt (#5473), up to a ceiling of 500 for pathological pages (PAGE_HIT_CEILINGindb/__init__.py); the row’s snippet is still the first. Pinned throughPOST /api/searchbytest_routes_search_page_hits.py.
Future (ideas, not scheduled)
- (#973) Book-native page numbers and chapter markers for book-shaped sources; backend detection feature, no current spec owner
- (#1493) Chapter jump-list atop Content tab from BookStructureNode; design-gated on thinking-layer #1488
- (#2418) Investigation of iOS/iPad text-editor parity with the Mac RTF editor; capability confirmed missing, not scheduled; Future of reader-view