Skip to content

Panes & Workspaces — Design Spec

Milestone: panes-workspaces Manual: TBD — a “Workspaces” section for Part I (Getting Started): what a workspace is, the five built-ins and their ⌘⌥1–5 shortcuts, how to split/close/resize a pane, and how the pane head’s left icon changes what a pane shows.

Design-led (Testing Constitution). The Fichero creative director owns this intent; tests enforce it; code makes them pass. One line per behavior, each to be cited by its pinning test. Status: DRAFT (most behaviors are [GAP]) — but the DESIGN DIRECTION is RATIFIED 2026-09-13 (see Design decisions). Grounded in a read-only code map + Findings F1–F7. Fix sequence: F5 reset [done] → F3 pin [done] → F6 plan==render [next] → then pane-list generalization (F7), legacy renderer retired, entities/claims unified. Tags: [OK] today · [BROKEN] regression, code contradicts the line · [GAP] intended, never built.

The finding in one line: the workspace is a half-finished migration to a pane-list model — the reliability fix is to finish it (one renderer, one vocabulary, a real pane list, per-instance pane state), not to patch symptoms. See F1–F7.

Changelog 2026-09-16 (dfa937946, CD live-testing pass). The always-a-PaneList step landed: activePaneList now SEEDS to Read, so the F7 PaneList path is always the render path and the legacy visibility-Bool fallback no longer renders. This closes the two-libraries / close-both / split-both cluster at the root (they were the legacy path showing through a nil-defaulted @State). Also shipped: the built-in set is now five (Read · Browse · Transcribe · Transcribe·Tall · Compare; ⌘⌥1–5 — Catalogue + Claims dropped, no separate Default Layout); the layout-recursion crash is fixed (WorkspaceSplitStack GeometryReader + clamp + single per-child frame); the Transcribe/Compare library strip is pinned to 72pt (PaneConfig.paneExtent); split-then-close-both is fixed (PaneHead close-ladder reorder); the kind-switcher is wired on the applied path (PaneList.changingLeafKind + \.paneKindSwitcher); \.isSolePane collapses a lone pane’s head close; pane heads are consistent liquid-glass (PaneFilterBar.showsSeparator default off, ChatView Divider removed); and the loupe now requires Option to be the sole modifier (so ⌘⌥1 no longer summons it). Pinned by BuiltInWorkspaceLayoutTests, MenuShortcutUniquenessTests, PaneInstanceIndependenceTests. The spec Status stays DRAFT.

Changelog 2026-09-18 (#4685/#4686/#4687 completion + adversarial-review fixes). The “one renderer, not yet one model” gap the 2026-09-17 residue note tracked is CLOSED: there is now exactly one pane-visibility model, one persistence funnel, and the dead widescreen-era code around both is deleted, not left dormant. - activePaneList is the ONLY source of pane visibility. The three legacy @SceneStorage Bools (showDocumentGrid/showDocumentCanvas/showReadingPane) are DELETED from ContentView.swift; paneVisibility (PaneVisibility.swift) is now a pure derivation of activePaneList.kinds — there is no separate storage that could disagree with what’s on screen. Every site that used to read a Bool directly (toolbar lit-state, cyclePaneFocus, paneAwareDetailMinWidth, currentPaneVisibilityPlan) now asks paneVisibility/ activePaneList. See panes.visibility.derived-from-list below. - One funnel, paneListDidChange(), both syncs and persists. Every writer of activePaneList (setPaneVisible, applyWorkspaceLayout, a saved-workspace apply, splitFocusedLeaf, pane-head close/kind-switch) ends by calling this ONE function (PaneVisibility.swift), which remembers the applied composition in WorkspaceLayoutDefaults (rememberPaneList) for the next launch. The seed (ContentView.activePaneList’s default) restores it — WorkspaceLayoutDefaults.rememberedPaneList() ?? BuiltInWorkspaceLayout.read.panes — falling back to Read when nothing was ever remembered. See workspaces.persist-applied-list below. - Saved workspaces carry the real composition. WindowLayoutSnapshot.paneList: PaneList? is the field that was missing (§”Existing machinery“‘s “model split” debt, closed): capture = activePaneList, apply = assign it. An old snapshot without the field decodes with paneList == nil and applies as the Read default (logged, not crashed); a PRESENT but malformed value ALSO degrades to nil rather than throwing out of decode and voiding the whole saved-workspace catalog (the SF10 adversarial-review finding). See panes.workspace.save/panes.workspace.reopen below. - Built-in layouts have STABLE leaf ids. BuiltInWorkspaceLayout.panes used to call the plain .leaf/.split factories (PaneList.swift), which mint a fresh random UUID() on EVERY access — so re-deriving the same built-in (navigating Read → Browse → Read, or a relaunch re-seeding against it) silently lost a dragged divider’s stored width and orphaned @SceneStorage keys, because WorkspaceSplitStack/PaneSpec key that per-instance state off a leaf’s id (the SF6 finding). Fixed at the source with PaneNode.stableLeaf/.stableSplit + UUID(stableName:) (deterministic, MD5-based) — every built-in composition now uses these, named "\(rawValue).<role>". Runtime mutations (toggling, splittingLeaf’s duplicate, settingVisible’s appended leaf) are UNCHANGED — they still mint fresh random ids, or two toggled-on panes of the same kind would collide instead of coexisting. See panes.builtin.stable-ids below. - A proportional split stays proportional until a real drag. WorkspaceSplitStack’s .fraction sizing used to convert to absolute points on first appear and write that into @SceneStorage (SF3 finding) — so a 0.4-fraction column seeded once at a 1000pt window stayed pinned to 400pt forever, even after the window (or the whole Mac) moved to a very different size. The eager seed is deleted; the divider’s binding is now a computed one whose getter is the freshly-resolved display value and whose setter (the only thing that writes stored state) fires exclusively from an actual drag. See panes.split.fraction-not-seeded below. - Menu Split routes through the model. SplitCommandRouting (the dead "<slot>-<kind>" key space the applied renderer never matched, #4685) is DELETED. The toolbar Workspaces menu’s Split Right/Below and the menu-bar Workspaces submenu’s twin now resolve the focused KIND (focusedPane ?? paneFocusHint) to activePaneList.leafIDs(of: kind).first and mutate via activePaneList.splittingLeaf(id, axis:) — symmetric with how close already worked via removingLeaf. Known limitation, not yet fixed: with two same-kind panes (Compare) this always resolves to the FIRST leaf of that kind, not necessarily the instance under the pointer — full per-instance precision is still the deferred “increment 4” this spec’s Migration order names. Every built-in today has at most one leaf per kind, so it’s exact for all of them. See panes.split.focused-only below (updated). - Deleted, confirmed callerless before removal: widescreenPaneSpecs (PaneSpec.swift), PaneList.fromVisibility (its only caller was widescreenPaneSpecs), the paneKindOverrides WRITE side (captureLayoutSnapshot/applyLayoutSnapshot — the live ContentView. paneKindOverrides dict has had zero readers since SplitCommandRouting went; the WindowLayoutSnapshot field itself stays, decode-only, so an old snapshot with the key still decodes), SplitCommandRouting, WindowLayoutPreset and WindowLayoutCommands.applyPreset (a second built-in-arrangement system parallel to BuiltInWorkspaceLayout, the exact workspaces.one-system violation below). Pinned by WorkspaceSystemBoundaryTests. testAppliedWorkspaceSplitIsWiredThroughThePaneListModel (un-skipped — it now finds a real caller of splittingLeaf(), WorkspaceLayoutDefaultsTests. testEveryActivePaneListWriterCallsTheOneFunnel (a source guardrail: every known activePaneList mutation site is followed by paneListDidChange()), WindowWorkspaceTests (paneList round-trip, malformed-decode leniency, dead-field cleanup), BuiltInWorkspaceLayoutTests (stable-id stability/uniqueness, splittingLeaf/removingLeaf still mint/preserve ids correctly), WorkspaceSplitStackSeedingTests (fraction re-resolves proportionally, not seeded).

This is an AREA spec for how a window is composed — the pane system that hosts every view mode (source/preview, transcription/words, entities, claims, graph/canvas, inspector). It sits above the per-mode specs (kg-tables, kg-entity-inspector, segment-representations) and owns the cross-pane concerns: split, per-pane configuration, synchronized magnification, and saving a composition as a workspace.

Intent (the design)

A window is a composition of panes, not a fixed source/detail pair. Each pane holds one view of the library — the page image, its transcribed words, the entities, the claims, a graph/canvas of related things, or an inspector — and each pane is independently configurable: its own view mode, its own zoom, its own magnifier state. Panes compose freely: three panes side by side, with the image shown in one and hidden in another; a source pane beside an entity pane beside a claims pane; or two source/preview panes for comparison. A composition can be saved as a workspace and reopened.

The reading surface is built for close looking. A page’s original image sits in one pane; its transcribed words sit in another and, on request, expand to fill their bounding box so the text occupies the same geometry as the ink. A magnifier (a zoom bar along the bottom of a pane) can follow the mouse, and magnification can be synchronized across panes so moving the loupe over the original moves it over the words too — or decoupled, one pane’s magnifier open while another’s is closed.

Entities and claims are not two bespoke screens; they are two views in the one pane system, sharing its selection grammar, split behavior, and workspace persistence. From an entity you can reach every source page it appears on; from a claim you can reach its sources. Selecting related things across panes composes a working set — related entities in a graph pane, their source pages in a preview pane, an inspector on the right — that is itself saveable.

Current architecture (grounded, 2026-09-13) — a half-finished migration

The workspace unreliability is not a pile of small bugs; it is one migration left half-done. A code map (every claim cited in Findings below) shows:

  • Two renderers for the centre. A newer pane-list model (PaneSpec list → widescreenPaneRow, each slot wrapped in its own SplittablePane) runs only in LayoutMode.widescreen (the default). .standard/.none fall to an older hand-branched renderer (centerContentRouting with PlatformVSplitView). Any behavior can diverge by layout mode. (F1)
  • Three vocabularies for the same panes. PaneVisibility/WorkspaceLayoutDefaults say grid/canvas/reading; PaneSpec.Kind says library/preview/reading/chat; WindowLayoutSnapshot says showLibraryPane/…. An invariant can’t be stated once. (F2)
  • Pin state lives at two different scopes. The Reader keeps pin inside ReadingPaneView (per split instance — correct); Preview and Library keep pin on ContentView, above the split, so both split halves share one pin. Same-looking control, opposite behavior. Entities/Claims have no pin at all. (F3)
  • Zoom is already per-instance (webZoom per ReadingPaneView, PDFZoomController per PDFPageWithToolbar) — no shared/global store exists. The “split shares one zoom” report is not explained by the code; it needs a live repro before any fix. (F4)
  • Claims don’t reset on library change. Entities read a per-library EntityStore from the environment; Claims cache a LibraryClaimsModel that captures the service once and reload on .task(id: folderId) — keyed to folder, never library — so the library-wide Claims row (folderId == nil on both sides of a switch) never refires. (F5)
  • Preview “isn’t always there.” PaneContentPlan claims preview content for .chat/.comparison/.workflow/…, but previewView renders only for .library (“the remaining #4525 step”); plan and implementation disagree. Plus .none mode and the width-collapse order shed the preview. (F6)
  • The pane model is fixed-slots, not a list. WidescreenPanePlan is four Bools and PaneSpec.Kind a fixed 4-case enum, so it cannot represent two of a kind — three previews side-by-side (original · words · reader) is impossible today; the only “second preview” trades the Reader slot away via paneKindOverrides. (F7)

The design: converge on one pane model

The north star is a single model, reached by finishing the migration — not a rewrite:

  1. One renderer. The pane-list path is canonical; retire centerContentRouting. Every layout mode composes the same PaneSpec list (a .none/.standard mode is just a different list, not a different renderer).
  2. One vocabulary. Collapse grid/canvas/reading and showLibraryPane/… onto PaneSpec.Kind; the visibility/persistence layers speak that one language.
  3. A pane LIST, not four Bools. Replace WidescreenPanePlan’s booleans with an ordered list of pane entries (kind + content binding), so a window can hold N panes including several of the same kind with different content — the enabler for “three previews: original · words · reader,” and for arbitrary compositions the reader saves as a workspace. Split (2×2 per slot) stays, but is no longer the only way to get a second pane of a kind.
  4. Per-instance pane state for every kind. Pin (and any future per-pane setting) lives inside the pane instance like the Reader already does, so split halves are independent for Preview and Library too, and Entities/Claims gain a consistent pin.
  5. Entities and Claims are one view system. Same data-lifecycle (per-library store, or both keyed on library id), same selection grammar, same reset-on-library-change, same pin — differing only in row content and open-target.
  6. Plan == render. A pane the plan says has content must actually render it, or the plan must not claim it (close the #4525 gap).

North star (CD, 2026-09-13): RELIABILITY over any specific layout

The overriding goal is a pane system that is reliable and works, so the CD can experiment with various layouts — not one hard-coded arrangement. Every finding below is a reliability defect (a pane vanishes, a toggle disappears, a split produces a state you didn’t ask for, a view “takes over”). Fixing these — one composition path, consistent toggles, predictable split, panes that never silently drop — matters MORE than delivering the Mail default. The Mail layout is just one composition the reliable system can express.

CD runtime review 2026-09-13 (design-lead testing) — findings from a live build

Findings from the creative director running the chat-in-sidebar build. Chat-in-sidebar itself works (committed c4a22c2b5). The rest are the workspace/pane defects to pin+fix.

  • panes.chat.toggle-in-sidebar-top — [GAP] (#4970) (→ #4705 increment 6; the side is superseded by #5292, panes.sidebar.title-bar-holds-sidebar-chat-and-add: Chat sits to the RIGHT) the chat show/hide toggle should sit at the TOP of the sidebar, beside the sidebar (panel) button — not the sparkles button in the main toolbar.
  • panes.sidebar-button.in-sidebar-section — [GAP] (#4735) the sidebar toggle button belongs IN the sidebar’s own top-left section (Xcode-style), not floating in the main window toolbar’s left group.
  • panes.split.asymmetric — [GAP/BROKEN] (#4669, #4737) splitting a preview vertically then horizontally makes a 2×2 grid of 4; the CD wants asymmetric nesting (“2 over 1” — two panes on top, one below). The current split caps at a symmetric 2×2 (SplittablePane.swift:156-166) and every sub-pane renders the same content. Needs nested/asymmetric split (part of F7).
  • panes.minimap-secondary-pane — [GAP] (#1932) a secondary split pane can act as a MINIMAP of the primary — a zoomed-out overview, especially for the WebKit/KG view. The split/side-by- side half of this request is substantially covered already (panes.split.asymmetric, panes.split.independent-mode-per-pane, panes.compose-three-plus) — the minimap-specific rendering mode is the part none of those name.
  • panes.library.horizontal-and-entities-parity — [OK] (fixed a7d349724) the Entities view no longer “takes over” — entity/claim/folder library selections all keep the same panes. Was: showsPreviewPane special-cased only entities→false (full-width takeover) while Claims kept the two-pane layout, violating the stable-panes policy. Fix: pure showsPreviewPane(viewMode: layoutMode:) with no selection input → parity structural. Pinned: ShowsPreviewPanePolicyTests. (Any remaining left-alignment detail is a follow-up once the takeover is gone.)
  • panes.toolbar.toggles-consistent — [OK] (fixed d4ab628fb) the pane toggles + Workspaces menu no longer vanish for KG collections. Was: both gated on supportsReadingWorkspace (.library && !isKGLibrarySelection), so Entities/Claims hid every toggle AND the recovery menu. Fix: pure showsPaneToggles(sidebarMode:compactFlow:) (drops isKG) + showsWorkspacesMenu (compact-only); Workspaces menu moved to its own un-gated conditional. Pinned: ToolbarTogglePolicyTests.
  • panes.claim.source-is-document — [OK] (fixed b7be99143) the Claims Source column shows the document NAME (resolved over ALL loaded docs, not just the folder scope — the id-fallback bug), is draggable (shared LibraryItemDrag payload), and clicks through to the source (the existing ClaimSourceRequest.request(for:) cursor entity statements use). Pinned: ClaimSourceLabelTests.

Legacy milestone fold — toolbar and shared chrome (#125 UX - Toolbars & Mini Toolbars,

#251 Surface Chrome - Shared Components; both milestones had no spec of their own)

The window toolbar (pane toggles, the breadcrumb/principal lozenge, the status island, the Workspaces menu icon) and the shared pane-head/footer chrome (MiniToolbar, PaneFilterBar) are already this spec’s surface — folding the two legacy toolbar milestones in here rather than starting a new one.

  • panes.filter-bar.shares-minitoolbar-height — [PARTIAL] (implemented and tested; #3370 still open pending close) every mini-toolbar-like pane strip — the sidebar bottom toolbar, the document-inspector annotation filter strip, the reader/library/preview mini-toolbars — shares ONE height and Liquid-Glass-compatible chrome across macOS/iPad/iOS, instead of PaneFilterBar hard-coding its own 24pt. Verified at HEAD: PaneFilterBar.height (Views/Components/PaneFilterBar.swift:32) reads MiniToolbar<EmptyView, EmptyView>.standardHeight directly — there is no separate constant left to drift — and both surfaces the issue named, SidebarBottomToolbar.swift and DocumentInspectorAnnotationsTab.swift, both build on PaneFilterBar. Pinned: MiniToolbarMetricPolicyTests.testPaneFilterBarUsesMiniToolbarHeight.
  • panes.status-island.separates-connection-and-activity — [PARTIAL] (#4536, #2450 — redirected from the legacy “Settings - Models & Providers” milestone while folding ai-settings.md’s pass 2, the same “Xcode-style activity status widget” ask) the status island should present backend connection, remote connections, WHO else is connected as a user, and activity as four SEPARATE indications, not one folded glyph+spinner. Verified at HEAD: two of the four already split out as their own toolbar items — EngineStatusToolbarItem and ActivityStatusToolbarItem — each with its own Liquid Glass section and popover, leaving StatusIslandToolbarItem for the message/selection line only (StatusIslandToolbarItem.swift:1-13, its own doc comment records the 2026-08-23 split). Still missing: a distinct “remote connections” indicator and a “who else is connected” indicator — multi-user/agent-session presence has no toolbar surface yet.
  • panes.status-island.message-budget — [PARTIAL] (implemented and tested; #4366 still open pending close) every island message reads completely at the island’s real width; nothing the app authors truncates mid-word. Verified at HEAD: StatusIslandMessage.budget/.declaredMaxWidth, .authoredMessages (the app-authored strings held to budget by test) and .shortForm(_:) (a named, tested seam that clips an OS/backend string on a word boundary, never a silent SwiftUI clip) all exist exactly as asked (StatusIslandToolbarItem.swift:156-215). Pinned: StatusIslandMessageBudgetTests.
  • panes.status-island.errors-are-short-and-typed — [PARTIAL] (#4269) the content area never shows raw error text (NSError descriptions, domains, codes, URLs); the island shows a short human sentence, and clicking it reveals the full technical text plus a one-click “Report to GitHub.” Verified at HEAD: StatusIslandMessage.resolve already routes every engine/import failure through shortForm(_:) before it reaches the island (StatusIslandToolbarItem.swift:244-266), so the SHORT-message half is built. Not verified on disk: a details-on-click popover showing the full text, and the “Report to GitHub” filing pipeline — no such view or endpoint call was found under Views/Shell/Toolbar/.
  • panes.status-island.selection-noun-matches-type — [PARTIAL] (#4586) the selection noun (“N images/pages/folders selected”) should pick off the SELECTION’s own file/doc types, matching PDFs-have-pages / folders-have-documents-or-images / images-are-images. Verified at HEAD: the noun-derivation closure (ContentView+Toolbar.swift:349-357) already checks fileType == .image before docType == .page, so an all-image selection should already read “images.” No test pins this derivation (StatusIslandToolbarTests only exercises resolve given an already-decided noun string), so whether the live repro the maintainer filed is actually fixed cannot be confirmed from source alone — flagged for a live re-check before this is retagged OK or closed.
  • panes.toolbar.owns-identity-namespace — [GAP] (#3203) every item contributing to a window toolbar should carry an explicit ToolbarItem(id:) from one shared namespace, so a prior duplicate-identifier class of launch crash (two earlier, already-closed P0 incidents) cannot recur, with a guardrail failing CI on a new id-less toolbar item. Verified at HEAD: only 2 of the 46 .toolbar { contribution sites in the tree declare any ToolbarItem(id:) (ContentView+Toolbar.swift, ContentView+InspectorContainer.swift) — the other 44 still rely on SwiftUI’s auto-derived identifiers. No guardrail script exists yet for this class.
  • panes.toolbar.ia-groups-by-what-it-acts-on — [GAP] (#4374) a toolbar control’s position should say what it acts on: the pane-visibility toggles (sidebar/reader/reading/ inspector) form one cluster at the window’s edge, and controls that act on the library (sort, filter, view-mode) sit over the library’s own mini-toolbar, not at the window’s far edge. Not verified as built; the View-menu-duplicating toolbar button this issue also flags is a menus-and-commands.md question, not repeated here.
  • panes.toolbar.breadcrumb-is-a-real-path — [GAP] (#4591, #4378) the breadcrumb should read as a drillable PATH (Library > Folder > PDF > 1) with a page SELECTION at the end, not a count prefix, and every element should be a real, draggable macOS proxy icon (file promises, so it still works when the server is remote). The existing breadcrumb/principal- lozenge behavior above this spec already owns is the format’s home; this issue is the richer interaction on top of it, not built.
  • panes.toolbar.declutters-per-item-actions — [GAP] (#2433) a main-toolbar button that only acts on the current selection (e.g. “open this attribute in a text window”) belongs in a contextual menu and/or a mini-toolbar icon on the thing itself, not the main window toolbar; “open in a new window” should be one general capability reachable for any node (doc/page/artifact/attribute), consistent with the reader/inspector mini-toolbars. Not verified as built.
  • panes.chrome.shared-surfacechrome-component — [GAP] (#3530) the Reader/Inspector tab bar + bottom mini-toolbar + sub-tab pattern should be extracted into one reusable SurfaceChrome component set so Workflow/Chat/Agent/Research/Search can adopt the same chrome without re-deriving it. Verified at HEAD: no SurfaceChrome-named type exists anywhere under fichero/fichero/ — MiniToolbar/PaneFilterBar are shared, but the tab-bar half of the pattern is not yet extracted.

Needs maintainer triage, not folded as a behavior here: #3540 (“DECISIONS NEEDED — surface-consistency, fourteen open questions for the maintainer”) asks which surfaces adopt SurfaceChrome and how (Workflow tabs, Chat/Agent tabs, Research’s 3-pane layout, Search’s tab strategy, the KG view-mode switcher’s location) — filed 2026-07-12, before the modes-to-panes and panes- workspaces rulings that have since answered several of its 14 questions on their own terms (the KG switcher question in particular looks pre-empted by panes.kg.select-shows-item- inspector and the modes-to-panes “KG graph/map = Library view modes” ruling). Re-reading it question-by-question against what has shipped since, rather than assuming it is still live in full, is a maintainer call, not one to make while folding a milestone.

Left in its legacy milestone, not folded here: #2501 (swipe-to-delete / row swipe actions on library and inspector list rows) is a Library/Inspector ROW-gesture ask, not window or pane chrome — no existing spec’s surface is the row itself (the library-view-modes spec proposed in the milestone ledger would be the right home once it exists). Left in #125 for now rather than forced into this spec.

Legacy milestone fold — window/pane redirects from “Library View” (#117)

Two more issues, redirected from the legacy “Library View” milestone while folding library-view-modes.md’s pass 2 (neither is a view-MODE question; both are window/pane behavior this spec already owns):

  • panes.double-click-focuses-current-window — [GAP] (#3364) double-clicking a sidebar/library item should focus/navigate in the CURRENT window by default; opening in a new window or tab stays reachable only from an explicit contextual command. Not verified as built.
  • panes.split.each-pane-its-own-document — [GAP] (#4565, #2422) a split reader pane should be independently targetable to a different document (drag a doc into a pane, or a per-pane picker), with a clear control choosing “different doc per split” versus the existing same-doc/compare mode. Not verified as built; ties this spec’s existing split-independence behaviors (panes.split.independent-mode-per-pane) to CONTENT independence, which those behaviors do not yet cover.

One more, redirected from the legacy “UX - Library & Reading Surface” milestone while folding library-view-modes.md’s pass 2 (a toolbar-chrome question, this spec’s territory, not a Library view-mode question):

  • panes.toolbar.reader-filter-button-is-explained-or-removed — [GAP] (#1473) the reading-surface top toolbar has a “filter” button whose purpose is unexplained — either give it a clear function and label, or remove it. Not verified either way.

One more, redirected from the legacy “Reader View - Page” milestone while folding reader-view.md’s pass 2 (the same toolbar-overflow territory panes.toolbar.* above already owns):

  • panes.toolbar.reader-overflow-collapses-before-overlapping — [GAP] (#2515) the Reader’s own toolbar (top and/or bottom strip) must stay within the reading column — never overlap the library sidebar/filmstrip or the inspector — and secondary tools should collapse into the trailing “…” overflow menu BEFORE anything overlaps, not after. Not verified as built; the issue’s own diagnosis (a ViewThatFits likely measuring the whole window rather than the content column) was not re-checked this pass.

Post-F7 design refinements (CD, 2026-09-14) — capture, revisit after F7

  • panes.kg.select-shows-item-inspector — [GAP, post-F7] (→ #4705 increment 7) clicking a claim or entity row auto-opens the full document inspector (with its source) today (works, but heavy). The CD wants selection to instead show a focused inspector for THAT claim/entity — the item’s own inspector, not the whole document+source surface. (Ties kg-entity-inspector; a claim inspector is the claim-side equivalent.)
  • panes.kg.clickable-lists-and-sidebar — [GAP, post-F7] (#4742) richer click-through interactions in the claims/entities lists AND the sidebar (click things to act/navigate). Deferred by the CD until F7 lands.
  • panes.content-column-under-sidebar — [BROKEN, F7] (#4743, re-diagnosed 2026-09-18: CANNOT CONFIRM from source, not retagged) (screenshots confirm as of 2026-09-14) the content column starts at the window’s LEFT EDGE (x=0) and runs UNDER the sidebar: with the sidebar shown, the leftmost columns are covered; hide the sidebar and the full content appears. CD 2026-09-14: this is NOT KG-specific — Claims, Entities, workflows, and images all do it, so it’s the general content-column placement in the renderer, not a per-view bug. Re-checked: the shell is a native NavigationSplitView (ContentView+RootLayout.swift) whose detail column (detailColumn) is a proper split-view slot with safe-area insets, not an absolutely-positioned overlay — structurally unlikely to bleed under the sidebar, and the F7 one-renderer migration this was filed against has since landed (activePaneList now always seeds to Read). But this is a rendered-pixel claim (“screenshots confirm”); source reading cannot prove a layout bug is gone. Manual check (ten seconds): open the app, select a Library/Claims/Entities/workflow item with the sidebar visible, and look at whether the content area’s left edge sits flush against the sidebar’s trailing edge or extends under it.
  • panes.vertical-no-breadcrumb — [BROKEN, F7] (#4744, re-diagnosed 2026-09-18: looks FIXED, by a different mechanism than the issue assumed — not retagged pending verify-close) the vertical split/pane has no breadcrumb bar (the horizontal one does). Re-checked: the per-pane clickable breadcrumb strip this issue describes was RETIRED entirely, not extended to the vertical case — ContentView+SidebarLayout.swift:194-199’s own comment: the pane-level strip was one of FOUR in-window copies of the same path and is retired (a dedupe); the location breadcrumb now lives ONLY in the window toolbar’s principal lozenge, which does not depend on split orientation at all. There is no longer a per-pane copy that could be present on one orientation and missing on another. Recommend verify-close.
  • panes.kg.filter-targets-active-view — [GAP] (#4745, re-diagnosed a SECOND time 2026-09-18: the mechanism named above was wrong; corrected below, awaiting a product decision, not a mechanical fix) TEXT search is NOT the divergence — traced symbol by symbol: the ONE per-window search request (activeSearchQuery) is passed to BOTH tables identically (LibraryView+ContentBranches.swift’s claimsContent/entitiesContent, searchQuery: activeSearchQuery on each); each table’s own local filterText is a deliberate in-table REFINE that intersects it, already pinned by EntitiesFilterTests/ ClaimsFilterTests. The REAL divergence is entity-KIND VISIBILITY, two mechanisms that never read each other: (A) the “Filter Entities” menu (LibraryView+EntityFiltering.swift) — a multi-select hide/show per kind, persisted app-wide and cross-window in @AppStorage("inspector.kg.hiddenKinds") — obeyed by the list view AND the inspector (KnowledgeGraphInspectorSection.swift reads the same key). (B) the Entities/Claims TABLES each keep their own private single-select @State filterType: String? (verified: EntitiesLibraryContent.swift:33, ClaimsLibraryContent.swift:43) and never read the hidden-kinds preference at all — so hiding a kind everywhere else still shows it in the table, and the table’s own type filter means nothing elsewhere. This is a product decision, not a bug to fix mechanically — three options: (a) additive, the tables also obey the hidden-kinds preference alongside their own refine; (b) one control, replacing the tables’ dropdown with the same multi-select; (c) declare the boundary intentional (a persisted “never show Dates” and a momentary “only People right now” are different intents). A separate, related question: the hidden-kinds preference is app-global @AppStorage today — with independent per-window/per-pane view modes coming (panes.split.independent-mode-per-pane, #4720), should it become per-window or per-pane instead? (Ties panes.kg.one-view-system above, → #4705 increment 3 — the underlying entities/claims unification is a separate, larger question this filter-visibility decision doesn’t have to wait for.)

Reliability sweep (same-class latent bugs, 2026-09-13 overnight) — F7/NEEDS-CD

A sweep for the same bug class as the fixed findings surfaced deeper, design-entangled defects (the toggle half of the CD’s “can’t turn preview/library/reader on or off”):

  • panes.toggles.inert-outside-widescreen — [OK, 2026-09-18] (fixed 73478d926, 9d428aee9, fe5b282c4) the Preview/Reader/Chat toolbar toggles are no longer no-ops outside widescreen — centerContentRouting (the layoutMode-gated legacy renderer this bug depended on) is DELETED (confirmed: only a doc comment referencing it remains, PaneSpec.swift:21), and the toggle buttons (ContentView+Toolbar.swift:186,195,207,256) read/write paneVisibility/ activePaneList directly — derived from the pane list, not gated by LayoutMode. A repo-wide search for currentLayoutMode = .widescreen (the force-reflow this line complained about) found zero remaining call sites. Pinned: ToolbarSurfaceLitStateTests (paneTogglesLight — the toolbar reads paneVisibility, not a layoutMode-gated Bool) — same fix cluster as panes.visibility.derived-from-list above.
  • panes.dead-toggle-policy — [OK] (bca344581, #4747 closed) ReadingWorkspacePaneTogglePolicy documented the exact intended behavior for the above (“a toggle from None/Standard enters the widescreen workspace and shows that pane”) but had zero call sites — the real toggle path reimplemented only its ON-half inline. Resolved by DELETION, not wiring — the type and its two self-only tests are removed entirely, matching the F7 plan’s own stated direction (the pane-list model’s toggling(kind:) is the one toggle path now, not a second documented policy nothing calls). Verified: git log’s own commit message confirms the removal; ReadingWorkspacePaneTogglePolicy no longer exists anywhere in fichero/fichero. The OFF-half the dead policy only documented is now covered by the SAME toggling(kind:) model every other toggle already runs through. Pinned: PaneListTests.toggleAbsentAppends, .togglePresentRemoves, .toggleTwiceRoundTrips, .toggleAlwaysChangesKinds.

These confirm the reliability root is F1/F7: the toolbar controls promise pane management the legacy renderer doesn’t deliver. The reliable fix is one composition path (F7), which is why “make it reliable so I can experiment” and “generalize to a pane list” are the same task.

The 2-column target (CD, restated): LEFT column = the browser (library / entities / claims) on top with a reader beneath it; RIGHT column = the preview (source image). Chat in the sidebar. This is the Mail default below, expressed in the eventual pane-list model.

F7 implementation plan (the reliability generalization) — for CD review

F7 is the one change that makes the pane system reliable AND lets you experiment with layouts. It subsumes the toggle-inertness (#1), the undocumented-vs-documented policy divergence (#2, resolved — bca344581 deleted the unwired policy), the widescreen-only pin/split (#3), the asymmetric split, and the 2-column target — because all of those are symptoms of two renderers + a fixed-slot plan. Incremental, not a rewrite; each step is independently shippable and testable.

  1. Model — a pane LIST, not four Bools. Introduce PaneList = [PaneEntry], where PaneEntry = { kind: PaneKind, scope: PaneScope, split: SplitSpec? }. PaneKind = library/preview/reading/chat (the existing PaneSpec.Kind). PaneScope = which library/document/folder the entry shows (this is what enables different libraries / different previews side by side). Replaces WidescreenPanePlan’s four showsXPane Bools. Pure + Codable → unit-testable and directly serializable as a workspace.
  2. One renderer. A single paneRow(from: PaneList) (generalise the existing widescreenPaneRow) renders EVERY layout mode. Retire centerContentRouting: .none and .standard become just shorter PaneLists (e.g. .none = [library], .standard = [library, preview]), not a different code path. Fixes #1 (all modes honor the list, so toggles work everywhere) and #3 (pin/split chrome is per-entry, so it’s available in every mode).
  3. Toggles mutate the list. A pane toggle adds/removes a PaneEntry of that kind (works in every mode; no more showDocumentGrid/showDocumentCanvas/… Bools — the list is the single source of truth). Deletes the showsPaneToggles/visiblePanes divergence and the dead ReadingWorkspacePaneTogglePolicy (#2). Pure seam: PaneList.toggling(kind:).
  4. Asymmetric split is a nested list. SplitSpec lets a PaneEntry hold sub-entries (a pane’s content is itself a small PaneList in a .horizontal/.vertical split), recursively — so “2 over 1” is split(.vertical, [split(.horizontal, [a, b]), c]). Replaces the symmetric 2×2 SplittablePane cap. Pure seam: the split tree + its flatten-to-views. Fixes the asymmetric-split finding.
  5. Per-instance state stays per-entry. Pin/zoom already live per sub-pane instance (F3/F4 done); each PaneEntry’s rendered view keeps that. The list just says which entries exist and how they nest.
  6. Persistence + workspaces. A saved workspace IS a PaneList (kinds + scopes + splits). WindowWorkspace snapshots become PaneLists. Delivers “save/reopen arbitrary compositions.”
  7. The Mail default is a starting list. sidebar+chat (left column, already done) · centre [library, reader] stacked · right [preview] — expressed as the default PaneList. No special layout code; just the initial value.

Sequence: (1) model + (3) toggling as pure Codable types with tests → (2) route the widescreen path through paneRow(from:) (it’s already list-shaped via PaneSpec) → fold .standard/.none in and retire centerContentRouting → (4) nested split → (6) workspace serialization → (7) default. Each step keeps the suite green; the risky view-composition steps (2, 4) need CD runtime verification, the model/logic steps (1, 3, 6) are unit-gated.

Default composition (Mail-style) — RATIFIED 2026-09-12

The default window is a three-region Mail-style layout. It prioritises the primary source image (full height, right) while grouping navigation and the AI workspace on the left and the browse→read flow down the centre.

┌────────────────┬─────────────────────────────┬──────────────────┐
│ Sidebar        │ Library browser             │                  │
│ (folder tree)  │ (icon/list — a HORIZONTAL   │                  │
│                │  thumbnail strip, Mail       │                  │
│                │  message-list style)         │   Source image   │
│                ├─────────────────────────────┤   (the archival  │
│ ─── divider ── │ Reader(s)                    │    scan, FULL    │
│ Chat history   │ (transcription / summary /   │    HEIGHT, far   │
│                │  metadata — one or two       │    right)        │
│                │  readers)                    │                  │
│ [ ask prompt ] │                              │                  │
└────────────────┴─────────────────────────────┴──────────────────┘
   left sidebar      centre: horizontal split       right column
  • Left — folder tree on top; chat history + its input stacked directly beneath it, in a collapsible split region (an NSSplitView divider the user can drag to give chat more room or collapse when just navigating). The chat input is attached to the chat history — it does not float at the bottom of the image/reader.
  • Centre — a horizontal split: the library browser (a horizontal thumbnail strip) on top, one or two readers (transcription / summary / metadata) below. Selecting a thumbnail above drives the reader below.
  • Right — the source image, anchored full height (historical documents are vertically oriented, so uninterrupted top-to-bottom space maximises zoom and minimises scrolling).

Behaviors (each → one pinning test)

A. Pane composition & split

  • panes.split.focused-only — [PARTIAL] splitting a pane (vertical or horizontal) splits the focused pane, not every column at once. Model + routing fixed (2026-09-15, 33cbcdf92): PaneList.splittingLeaf(id:axis:) splits only the targeted leaf, and PaneSpec.slot makes each pane’s split key per-instance (was per-kind), so same-kind panes no longer share one split cell. 2026-09-18: the toolbar Workspaces menu’s Split Right/Below and its menu-bar twin now ACTUALLY route through the stored PaneList (fixed, closed) — resolving focusedPane ?? paneFocusHint to a kind, then activePaneList.leafIDs(of: kind).first, then activePaneList.splittingLeaf(id, axis:). REMAINING (#4795): instance-precise focus — with two same-kind panes (Compare) this still targets the FIRST leaf of the focused kind, not necessarily the instance under the pointer; every built-in today has at most one leaf per kind, so it’s exact for all of them, but full per-instance precision (the “increment 4” this spec’s Migration order names) is still open. Pinned: PaneListTests (“splitting a pane splits ONLY that pane…”) + WorkspaceSystemBoundaryTests. testAppliedWorkspaceSplitIsWiredThroughThePaneListModel (un-skipped 2026-09-18 — it now finds a real caller of splittingLeaf( outside the model’s own file).
  • panes.split.opens-the-complement — [GAP] (#4977) a split opens the pane that complements the one split, not a copy of it, following the Library → Preview → Reader chain (a Library opens a Preview, and so on), except Preview itself, which splits to a second Preview (for comparing two pages). A new split pane joins its parent’s link group (panes.link.colour-chip). (Ruled 2026-10-04.) Today splittingLeaf duplicates the leaf’s kind.
  • panes.close.this-pane-only — [OK] (applied path always live 2026-09-16, dfa937946) closing a pane removes only that pane; its siblings survive and a split that loses a child collapses to the survivor, not the whole row. The VIEW now always renders the stored PaneList (seed = Read), so the close routes through \.paneCloseAction → removingLeaf(id:) rather than the legacy setPaneVisible Bool. Split-then-close-both is fixed by reordering the PaneHead close ladder so an active in-slot split collapses by one before the leaf is removed. Pinned: Tests/Unit/general/Models/PaneListTests.swift (“closing a pane removes ONLY that pane…”, “…collapses to the survivor — the row does not disappear”) + PaneInstanceIndependenceTests.
  • panes.head.kind-switcher-everywhere — [PARTIAL] (#4706; Reader fixed d8621ecc3, pinned by PaneHeadKindSwitcherParityTests; Chat follows → #4705 increment 6) every pane head that renders a leaf kind mounts the kind selector, so ANY pane can become a Library, Source or Reader. Cause (verified): the Reader head mounts PaneKindSelector with collapsesKindIntoLens: true (the one-icon ruling), whose merged-lens path never consulted \.paneKindSwitcher; Library / Preview / Chat take the adaptive path, which does. Every leaf already receives the switcher (PaneSpec.swift:318-321). Placeholder kinds (.inspector, .chat) stay out of the list until they are real leaves (#4705, increments 6–7); the chat head gains the selector when chat becomes a pane. Test: a source guardrail that every pane head mounts PaneKindSelector, plus a pure test of selectableKinds.
  • panes.split.independent-mode-per-pane — [BROKEN] (#4720) each pane holds its own view mode; changing one pane to Entities or Claims does not clear or convert the others. Today switching a pane’s node-type to entity/claim in the entities view removes them from the other panes.
  • panes.open-view-arbitrarily — [GAP] (#4722) any pane can be set to any view mode (source / words / entities / claims / graph / inspector) directly, without routing through a document selection. Today there is no way to open an entity or claim view arbitrarily in a window.
  • panes.compose-three-plus — [GAP] (#4724) a window supports three or more panes, and any pane may hide its image while another shows it.
  • panes.visibility.derived-from-list — [OK, 2026-09-18] (#4525) which content panes show (library/preview/reading) is a PURE derivation of activePaneList.kinds (PaneVisibility.paneVisibility, PaneVisibility.swift) — not a separate stored Bool per pane. The three legacy @SceneStorage Bools this used to read are DELETED from ContentView.swift, so there is nothing left for toolbar labels, View-menu checkmarks, or cyclePaneFocus’s pane-cycle order to drift out of sync with what the window actually renders. Pinned: WorkspaceLayoutDefaultsTests (testOnlyTheDeliberatelyChosenSurfacesAreRemembered — the remembered-Bool inventory is chat + layoutMode only, not the three panes) + ToolbarSurfaceLitStateTests (paneTogglesLight — the toolbar reads paneVisibility, not a Bool).
  • panes.builtin.stable-ids — [OK, 2026-09-18] a built-in workspace’s leaf ids are STABLE across every access (same PaneList value every time BuiltInWorkspaceLayout.read.panes is read), not freshly random each time. PaneNode.stableLeaf/.stableSplit (PaneList.swift, UUID(stableName:), MD5-based) replace the plain .leaf/.split factories in every built-in composition (BuiltInWorkspaceLayout.swift); a leaf’s name is "\(rawValue).<role>", unique within a case and across cases. Without this, navigating away from and back to a built-in (or a relaunch reseeding against it) silently lost a dragged divider’s stored width and orphaned @SceneStorage keys, because WorkspaceSplitStack/PaneSpec key that per-instance state off a leaf’s id. Runtime mutation (toggling, splittingLeaf’s duplicate, settingVisible’s appended leaf) still mints fresh random ids — a stable id there would make two toggled-on panes of the same kind collide instead of coexisting. Pinned: BuiltInWorkspaceLayoutTests (stability across two accesses, no id shared between built-ins, splittingLeaf still mints a fresh id for the new duplicate, removingLeaf leaves survivors’ ids untouched).
  • panes.split.fraction-not-seeded — [OK, 2026-09-18] a .fraction-sized split column (WorkspaceSplitStack) stays proportional to the CURRENT window size until the user actually drags its divider — it is never converted to an absolute-points value just because the window happened to render once. Before this, seedIfNeeded wrote fraction × total into @SceneStorage on the first .onAppear, so a 0.4-fraction column seeded at a 1000pt window stayed pinned to 400pt forever, even after the window (or the whole Mac) moved to a very different size. Fixed by deleting the eager seed and making the divider’s binding computed: its getter is the freshly-resolved display value (WorkspaceSplitStack.resolvedExtents, already unset-aware); its setter — the only thing that ever writes stored state — fires exclusively from ResizableDivider’s own drag handler. Pinned: WorkspaceSplitStackSeedingTests (an unseeded fraction re-resolves proportionally across two different totals; a real stored override still wins regardless of total).
  • panes.split.peers-open-even — [BROKEN] (retagged 2026-09-19 from the maintainer’s own test session — was [PARTIAL]; #4849) confirmed live, not merely unconfirmed: in Transcribe the three columns above the film strip are NOT balanced — the first preview’s image overflows into the second column. The lesson, stated plainly: the pinning tests below passed and this behavior still failed on screen, because they are pure sizing-math functions — none of them mounted a pane or rendered a real Transcribe workspace. A green test proved the math is right for the inputs it was given; it never proved those are the inputs a real pane produces. The rule stated precisely: when a horizontal split has 2, 3, or 4 SIBLING peer panes and the user has not yet dragged a divider, the columns open EQUAL — each is 1/n of the row. A user’s drag still wins afterwards, same as panes.split.fraction-not-seeded above. A deliberately UNEQUAL built-in layout (for example a narrow Library navigator beside wider content) is not a peer split and keeps its own specified weights — the even rule is the default for PEERS, not a blanket override of every split. Fixed: siblings with no explicit fraction or extent are now identified as PEERS and share what the explicit ones leave, evenly, for 2/3/4 peers alike, in a pure function tested at each count; a deliberately weighted pane keeps its weight, and a stored drag still wins. WorkspaceSplitStack gained a third stored divider slot so four peers have the three resizable columns they need — additive, existing stored layouts read exactly as before. Two things recorded, not treated as follow-on bugs: the Read workspace’s top-level split had no weight on either side and came out 40/60 under the old fallback; under the peer rule it is now an even 50/50 (Read’s own inner 0.4 split, and every other built-in, resolve unchanged). And a pin with MORE THAN ONE peer in the same split does NOT divide evenly — a known, tested limitation, not an oversight: the pin’s own points aren’t known at this position-only sizing step, so its non-flexing peers divide the WHOLE total, not total-minus-pin. No built-in layout hits this shape today. PARTIAL rather than OK for the same reason as the strip-extent behavior above: verified against the sizing math, not a rendered pane. Pinned: WorkspaceSplitStackSizingTests.peersOfEveryCountOpenEqual, .mixedSplitKeepsExplicitWeightAndSharesTheRest, .pinnedSplitKeepsItsPointsAndItsOnePeerFlexesExactly, .pinnedSplitWithMultiplePeersIsAKnownLimitation, .storedDragWinsOverPeerDefault, .noPeersKeepsEveryExplicitPreference (same file/suite as above).
  • panes.strip.fixed-extent-is-content-not-whole-pane — [BROKEN] (retagged 2026-09-19 from the maintainer’s own test session — was [PARTIAL]; #4848) confirmed live, not merely unconfirmed: the Transcribe film strip is still too short — no drag bar exists between it and the panes above either (a related, separate finding, C1). The lesson, stated plainly: the pinning test below passed and this behavior still failed on screen; the commit’s own note already said “not yet seen on screen: no preview render was possible” — that caveat was correct, and the maintainer’s test is exactly the render it was waiting on. A HARD-pinned pane extent (PaneConfig(paneExtent:)) describes the visible CONTENT the user is meant to see — a strip of page icons, say — not the whole pane including its own head and footer chrome. Fixed: 72 still means the visible icon strip, unchanged in the built-in layouts; the sizing layer now adds the pane’s own chrome on top, summed from the SAME metrics the pane head and mini toolbar render at (no second magic number), scoped to Library leaves only. Stored drag state is still ignored by this addition, as before. PARTIAL rather than OK: the commit’s own note says “not yet seen on screen: no preview render was possible” — the fix is verified against the sizing math, not against a rendered pane. Pinned: WorkspaceSplitStackSizingTests.libraryStripExtentAddsChrome (file fichero/Tests/Unit/general/Models/WorkspaceSplitStackSizingTests.swift, suite WorkspaceSplitStackSizingTests).
  • panes.split.minimap — [GAP] (#1932) a split pane can act as a minimap of another pane’s content — especially the WebKit/KG graph view, where a small secondary pane shows an overview of the whole document/graph while the main pane is zoomed in. Splitting and side-by-side comparison themselves are already covered above (panes.split.focused-only, .independent-mode-per-pane, .fraction-not-seeded) — this behavior is specifically the MINIMAP relationship between two panes, not plain splitting. Distinct from NavigatorMiniMap.swift (Views/Preview/ImageViewer/), which is an image-viewer zoom navigator inside a single pane (preview-magnifier.md’s territory) — not a second PANE showing an overview of a first one. Not built.
  • panes.options-per-pane — [PARTIAL] (#5233, #4689) (→ #5280, ruled 2026-10-01) each pane remembers its own filter and metadata settings: two panes of one kind set different values and neither overwrites the other, and both survive a relaunch. A new pane starts from the last choice made anywhere, as a new Finder window does (PaneStorage: a pane-id → value map beside the shared value; pane leaf ids are saved with the pane list). Built so far: the Library’s Metadata attributes, content lines and Show kind (05c3641ad); the Preview’s layer toggles (ef8bc076d); the Library’s column visibility, sort by folder, dates lozenge and entity-kind filter (pinned by fichero/Tests/Unit/general/Views/Library/LibraryOptionsPerPaneTests.swift, which drives the real wrapper in two hosted panes and reads it back as a relaunch; the kind filter answers the hidden-kinds question above: per pane, and the Inspector follows the last change made). Still per window: the table’s column order and widths (TableColumnCustomization); still app-wide: icon scale, canvas colour-by and arrangement, the magnifier and loupe, and the Reader and Inspector toggles.

B. Cross-pane zoom & magnifier state

SCOPE (creative-director, 2026-09-17): the magnifier/loupe ITSELF is a Preview / source feature — how it tracks the pointer, magnifies, parks and resizes belongs to [[preview-magnifier]], not here. What is a PANE concern is only how that state behaves across MORE THAN ONE pane: whether each pane holds its own, and whether they sync. This spec owns the plumbing; the preview spec owns the instrument.

  • panes.magnifier.per-pane-open-state — [GAP] (#5015, #4725) each pane’s magnifier opens and closes independently — one pane magnified while another is not. (Pane-scoped state; the magnifier’s own behavior is preview-magnifier.)
  • panes.zoom.sync-across-panes — [GAP] (#4726) when synchronization is on, zoom/magnification in one pane drives the corresponding region in the others (original ↔ words), so the loupe is shared; sync is toggleable, off by default. This is the ONE genuinely cross-pane magnifier behavior — it cannot live in the preview spec because it is about panes relating to each other.
  • panes.words.fill-bounding-box — [GAP] (#4728) on request, transcribed words expand to fill their segment’s bounding box, occupying the same geometry as the underlying ink. Owned by segment-representations (the text representation rendered into the segment anchor); listed here only because it is observed in a pane.

C. Unified entity ↔ claims view system

  • panes.kg.one-view-system — [BROKEN] (→ #4705 increment 3) the entities view and the claims view are the same pane-system view, sharing selection grammar, split, magnifier, and workspace persistence. Today they are separate implementations that behave differently.
  • panes.kg.library-change-resets — [OK] (fixed 5b709aca0) switching the active library resets the claims AND entities tables to the new library’s data — Claims key .task on a composite library|folder key and rebuild the cached model on a library switch; Entities key on ObjectIdentifier(store). Pinned: ClaimsLibraryReloadKeyTests (fichero/Tests/Unit/general/Views/Library/ClaimsLibraryReloadKeyTests.swift).
  • panes.entity.sources-pane — [GAP] (#4729) an entity pane can show all source pages the entity appears on — scroll through them, see the same name across four documents, judge whether it is one person. (An entity is a name; its statements/sources are where it lives — see kg-entity-inspector.)
  • panes.claim.sources-pane — [GAP] (#4730) a claim (or a page’s set of claims) can show its source pages in a preview pane, each anchored to the passage.
  • panes.inspector-always-visible — [GAP] (#1199) the inspector is a stable, always-present rightmost pane across every view (library, reading, KG graph, workflow) — never hidden, never replaced by a takeover. Not built: .inspector is a PLACEHOLDER kind today, not a real leaf (panes.head.kind-switcher-everywhere’s own text: “placeholder kinds (.inspector, .chat) stay out of the list until they are real leaves”); the spec’s own Migration order already names “Inspector as a PaneKind” as a planned step (§Migration item 3) — this behavior is that step’s acceptance criterion, not new scope.
  • panes.inspector-chrome-icon-tabs — [GAP] (#1854) every right-hand inspector surface (document inspector, WebKit/KG view, image/preview) renders its tabs as a compact SF-Symbols icon tab-bar (Xcode-style) with a centered “No Selection” placeholder when nothing is selected, rather than each surface inventing its own chrome. Pure presentation, not a content change — distinct from panes.inspector-always-visible above (whether the inspector exists at all vs. how its own tabs look once it does).

D. Workspaces

  • panes.workspace.crud-contract — [OK] (verify-close candidate; TL-2’s original ask, filed before this backend existed) a workspace’s curated_items support atomic add/remove/reorder and resolve their aliases to full objects, library-canonical (never copied). Built: PATCH /{doc_id}/workspace (documents.py:997) and GET /{doc_id}/workspace/items (documents.py:1015, resolving curated_items via _normalize_curated_items) both exist. Pinned: test_routes_documents_workspace.py::test_workspace_patch_add_remove_reorder_items, ::test_workspace_items_resolve_document_alias_targets, ::test_list_workspaces_returns_only_workspace_docs, ::test_document_and_agent_workspaces_have_distinct_endpoints.
  • panes.workspace.save — [PARTIAL] “Save Current as Workspace…” captures the REAL pane composition, not a lie: WindowLayoutSnapshot.paneList: PaneList? (WindowWorkspace.swift) is set to activePaneList (captureLayoutSnapshot, ContentView+LayoutChooser.swift) — this was the field that was missing (§”Existing machinery“‘s “model split” debt); pinned by WindowWorkspaceTests.testSnapshotCarriesTheAppliedPaneListThroughJSON (round-trip incl. leaf ids). REMAINING (#4731): sync-toggle state and a live selection-scope snapshot beyond PaneScope.documentId are not captured — deliberately out of scope for this increment (spec §”Resolved 2026-09-15”: save = layout only, by design), tracked as a real backlog item rather than closed as won’t-fix.
  • panes.workspace.reopen — [OK] applying a saved workspace assigns its paneList to activePaneList (applyLayoutSnapshot) — before this fix it never touched activePaneList at all, so applying a saved workspace changed nothing visible. An OLD snapshot (saved before this field existed — a brand-new coding key, so old JSON simply lacks it) decodes with paneList == nil and applies as the Read default, logged rather than crashed. A PRESENT but MALFORMED value (the SF10 adversarial-review finding) also degrades to nil — via a local try? around the one field’s decode inside WindowLayoutSnapshot.init(from:) — rather than throwing out of decode and voiding the ENTIRE saved-workspace catalog (WindowWorkspaceCatalog.decoded(from:) swallows any throw from a member’s decode to nil for the whole catalog). Pinned: WindowWorkspaceTests (testAnOldShapeSnapshotDecodesWithPaneListNil, testASnapshotWithMalformedPaneListDataStillDecodesWithPaneListNil, testOneWorkspaceWithMalformedPaneListDoesNotDeleteTheRestOfTheCatalog).
  • workspaces.persist-applied-list — [OK, 2026-09-18] the applied PaneList survives a relaunch: EVERY writer of activePaneList (setPaneVisible, applyWorkspaceLayout, a saved-workspace apply, splitFocusedLeaf, pane-head close/kind-switch) ends by calling PaneVisibility.paneListDidChange(), the ONE funnel, which remembers the composition via WorkspaceLayoutDefaults.rememberPaneList (a JSON-encoded UserDefaults key, kept OUTSIDE the Bool-only WorkspaceLayoutDefaults.Key enum since it’s a different shape). ContentView. activePaneList’s seed reads it back — WorkspaceLayoutDefaults.rememberedPaneList() ?? BuiltInWorkspaceLayout.read.panes — falling back to Read when nothing was ever remembered. Pinned: WorkspaceLayoutDefaultsTests (testRememberedPaneListRoundTripsThroughUserDefaults, testNoRememberedPaneListReturnsNilSoTheCallerCanFallBackToRead, testThePaneListSeedIsWired, and the structural guardrail testEveryActivePaneListWriterCallsTheOneFunnel — every known mutation site is followed by paneListDidChange() within a few lines — a new writer that skips it fails this test by name).

E. Default layout & chat placement

  • panes.layout.mail-default — [OK] (seed built 2026-09-16, dfa937946; pinned BuiltInWorkspaceLayoutTests) a fresh window now seeds a real built-in PaneList — activePaneList defaults to Read — so the window always opens in a composed workspace via the F7 path (no more nil-defaulted legacy fallback). The specific Mail-style composition below is superseded as the seed by Read (library+reader beside a full-height preview); the sidebar+chat left column and full-height right source hold, but the exact centre split described here is the Browse/Read arrangement, not a distinct “Mail” default. Pinned: BuiltInWorkspaceLayoutTests (Read is the default; one library leaf over reader, beside preview).
  • panes.chat.below-sidebar — [BROKEN] (→ #4705 increment 6) the chat history and its input live in the left sidebar beneath the folder tree, in a collapsible split region; the input is attached to the chat history. Today the chat prompt sits at the bottom of the centre column, under the image/reader, rather than under the chat text.
  • panes.chat.collapsible-split — [GAP] (→ #4705 increment 6) the sidebar↔chat divider drags to resize and collapses the chat region when only navigating.
  • panes.library.horizontal-icon-strip — [BROKEN/GAP] (#4732, #1856 — now folded onto this milestone, same duplicate request) the library browser renders as a horizontal thumbnail strip (icon/list, Mail message-list style) at the top of the centre column (“I want the icon view back — horizontal, like in Mail”). #1856 additionally frames this as a display-mode alongside grid/list/table/map (LibraryView+DisplayModes.swift, LibraryView+TableMapViews.swift) with its own toolbar/View-menu toggle and per-window @SceneStorage — the same capability #4732 already tracks here, not a second one.
  • panes.reader.one-or-two-below-browser — [GAP] (#4733) below the browser strip sit one or two readers (transcription / summary / metadata), driven by the browser selection.
  • panes.source.full-height-right — [GAP] (#4734) the source image occupies the full-height right column by default.

F. Maintainer test, 2026-09-19 morning

Live-build findings from the maintainer’s own test session (ten screenshots), paraphrased — paired with the panes.strip.fixed-extent-is-content-not-whole-pane / panes.split.peers-open-even retags below, since both are the same “tested against sizing math, not a mounted pane” lesson.

  • panes.model.per-pane-scope-and-kind-unread — [BROKEN] (#4887) the pane MODEL already carries per-pane fields — PaneScope (libraryId, documentId, folderId) and PaneConfig.libraryContentKind (both fichero/fichero/Models/PaneList.swift) — but nothing in the LIVE rendering reads them. Verified on disk: PaneConfig.libraryContentKind (PaneList.swift:79) is written at init and read in exactly one place outside its own struct — WorkspaceLayoutPreview.swift:95, a layout-preview thumbnail label, never the live LibraryView. LibraryView keeps its OWN, separate @State var libraryContentKind (LibraryView.swift:337), which only takes effect when effectiveContentKind (LibraryView+ContentBranches.swift:176-181) falls through to it for .documents; whenever a KG collection is selected, effectiveContentKind switches on contentCollection instead. contentCollection (LibraryView.swift:22) is passed from the ONE production LibraryView(...) construction site (ContentView+Navigation.swift:39-47), fed by sidebarContentCollection (ContentView+StateLayout.swift:133-138) — a WINDOW-scoped property driven by selectedKnowledgeKind, also window-scoped. PaneScope.libraryId (PaneList.swift:53) has zero reads anywhere outside its own init — every actual library-scoping read in the app goes through windowState.libraryId (Models/WindowState.swift), one window-scoped value, grepped across the whole Views/ tree. Net effect: every Library pane in a window follows ONE window-wide sidebar selection (content kind and library alike) — the per-pane fields the model already declares are decorative. This is the DESIGN ROOT behind, at least in part, panes.split.changing-one-leaf-kind-changes-whole-column and library.modes.persists-until-changed (library-view-modes.md) below, and is the prerequisite panes.library.not-linked-to-each-other / panes.library.explicit-link-to-one-preview / panes.library.drag-to-connect depend on — panes cannot be linked or unlinked while their live state is one window-wide value with no per-instance seam wired to the model field that already exists for it. Not built.
  • panes.transcribe.strip-has-no-drag-bar — [GAP] (#4876) the Transcribe workspace has no draggable divider between the bottom film strip and the panes stacked above it — every other split in the pane system is a real ResizableDivider; this boundary is not. Not built.
  • panes.transcribe.horizontal-two-rows-vertical-strip-left — [GAP, RULED] (#4877) the workspace currently named “Transcribe · Tall” is wrong; the maintainer’s ruling renames and reshapes it to “Transcribe Horizontal”: TWO ROWS, with a VERTICAL film strip on the LEFT — the layout for working on a scroll (a long, narrow document). Supersedes the “Transcribe · Tall” row in §”The built-in workspaces” table above. Not built as ruled.
  • panes.browse.library-narrow-left — [GAP, RULED] (#4877) the Browse workspace’s Library pane should be narrow, docked to the left edge — not the wider icon-strip composition the table above currently states. Not built as ruled.
  • panes.compare.two-images-plus-inspector — [GAP, RULED] (#4877) the Compare workspace should be two columns of images side by side, plus the Inspector as the third element — replacing the current composition’s reading pane. Not built as ruled.
  • panes.builtin.read-unchanged — [OK] the Read workspace ([ library(docs,table) · reading ] beside preview(image), §”The built-in workspaces” table above) is right as built; the maintainer confirmed it on screen and ruled it should not be touched. Pinned: BuiltInWorkspaceLayoutTests (Read composes exactly ONE library leaf, per the table above).
  • panes.split.changing-one-leaf-kind-changes-whole-column — [BROKEN] (#4878) splitting a Preview pane into two and changing the BOTTOM pane’s kind to Library changes the ENTIRE column, not only the bottom pane, and the pane footers stack (screenshot 8.30.25). Seen on screen this morning; expected is that changing one split half’s kind changes only that pane. Prerequisite: panes.model.per-pane-scope-and-kind-unread above — a pane’s kind/content cannot be independent of its siblings while the live state one window-wide value drives is the thing changing.
  • panes.library.narrow-pushes-content-off-edge — [BROKEN] (#4966, #4879) narrowing the left Library pane pushes its content off the left edge of the window instead of reflowing/ shrinking it to the new width (screenshot 9.11.46). Seen on screen this morning.
  • panes.chrome.head-and-footer-not-shared — [BROKEN] (#4880) pane head bars and pane footer bars do not share one component: the Library’s head/footer are slightly taller than Preview’s and Reader’s, and the Library’s kind chooser has no down arrow while Preview’s and Reader’s do (screenshot 9.12.08). Seen on screen this morning; expected is one shared component so height and affordances are tuned in one place.
  • panes.preview.pdf-click-reloads — [BROKEN] (#4558) in a PDF, clicking something reloads the Preview rather than moving within the already-loaded document. Reproduced again this morning (see #4558’s existing symptoms 2/3 and root-cause hypothesis).
  • panes.library.not-linked-to-each-other — [GAP, RULED] (#4881) opening something in one Library pane does not change another Library pane — true today because no cross-library-pane linking mechanism exists at all (every Library pane resolves its own selection independently). The maintainer’s ruling (2026-09-19) confirms this is the CORRECT behavior going forward, not a gap to close by adding cross-linking — but no test pins the independence as a deliberate invariant today (untested-true, not untested-broken), so this stays GAP until a pinning test exists; the gap that needs BUILDING is the explicit linking D2/D3 below describe, not removing this independence. Prerequisite: panes.model.per-pane-scope-and-kind-unread above.
  • panes.library.explicit-link-to-one-preview — [GAP, RULED] (#4881) the maintainer needs one Library pane linked to one Preview pane, and a second Library pane linked to a second Preview pane, such that changing the source shown in one linked pair never changes the other. No such linking mechanism exists today — see panes.model.per-pane-scope-and-kind- unread above for the verified reason: PaneScope.libraryId exists on the model but nothing reads it to establish or display a link between two panes. Related: panes.split.each-pane-its-own-document (#2422, content independence within one split) and #4666 (color-coding a linked group, the visual half).
  • panes.library.drag-to-connect — [GAP, DESIGN] (#4881, #4666) proposed by the maintainer, not decided: drag an item from a Library pane onto a Source pane to open it there AND connect the two panes; the same gesture for an Inspector entity or claim. A light color shows an active connection. Prerequisite: panes.model.per-pane-scope-and-kind-unread above. Open questions, listed not decided: how a connection is broken or re-targeted; whether a Library can be linked to more than one Preview at once; whether the connection persists in a saved workspace; how the color assignment avoids clashing across more than a few simultaneous links.

  • panes.environment-boundary-covers-every-hosted-pane — [PARTIAL] (#4682, #4897) every applied pane’s hosting boundary injects the FULL set of window/app @Environment/@State objects a pane subtree might read, not a hand-picked subset per boundary. Built: WindowEnvironmentModifier (f88a031e8) is now the ONE shared list applied at all three hosting boundaries (PaneSpec, ContentView+Navigation, ContentView+RootLayout) — previously each boundary hand-picked its own list, and WorkflowStore was missed by all three. It was broken TWICE before this: first, a pane leaf’s re-injection was missing entirely (07e712157, launch crash); then even after boundary-by-boundary re-injection landed, the three boundaries’ hand-picked lists still disagreed (f88a031e8 unified them). PARTIAL because nothing tests either fix: no automated test mounts a pane subtree at any of the three boundaries and asserts every environment object it might read resolves without a trap — a Mirror-based reflection test comparing WindowEnvironmentModifier’s property list against ContentView’s own list would catch “forgot to add a new one” but NOT “the object doesn’t actually resolve when a real pane is hosted,” since that failure is in SwiftUI’s real AttributeGraph resolution, not in a property list’s contents. The test must build (or mount) a real pane view at each of the three boundaries with the shared modifier applied and assert no trap when reading each environment object — a reflection-only test would keep passing if a boundary silently stopped applying the modifier at all.

  • panes.scene-roots-carry-a-fallback-environment — [PARTIAL] (#4897) every top-level Window/WindowGroup scene root that can reach workflow-execution UI carries a fallback WorkflowExecutionObserver, not only the scenes someone remembered to wire. Built: 54e01872f added the fallback to the five scene roots that lacked it (About, Feature Tier Legend, Install Tools, Document, Language Coverage). PARTIAL because nothing tests it: no test instantiates each scene root view and asserts it resolves the observer with or without the app’s own injection. The test shape that WOULD catch a regression: an NSHostingController-based test per scene root, built once WITHOUT the app’s own .environment(appExecutionObserver) (asserting the fallback alone is enough) and once WITH it (asserting no duplicate/conflicting instance) — narrower than a full app launch, no scene restoration needed. A pure structural/reflection test over FicheroApp.swift’s scene bodies (every Window/WindowGroup applies the modifier) would catch “forgot to wire a new scene” but not “the fallback itself fails to resolve when actually hosted.”
  • panes.toolbar-inspector-survive-restored-scene-state — [PARTIAL] (#4774, #4897) the toolbar and inspector, which do NOT inherit their hosting window’s environment the way an ordinary pane subtree does, must read WorkflowExecutionObserver optionally rather than trapping when a RESTORED scene (a relaunch with saved window state) mounts them before the window’s own injection completes. Built: 48498334c made five sites (StatusIslandToolbarItem, ActivityStatusToolbarItem, ArtifactsInspectorPane, ArtifactEntityViews ×2, WorkflowInspector) read the observer as optional. It was broken once: the crash reproduced only with an actual restored document-selection scene state, ~25s after a relaunch — not on a cold launch with no saved state, which is why the fix’s own commit says it was never build-verified by the automated gate, only reproduced by hand in Xcode. PARTIAL because nothing tests it, and this is the hardest of the three panes fixes to test: a Mirror/reflection test cannot see this failure at all (it is a .toolbar/.inspector environment-non-inheritance property of AppKit/SwiftUI, not a missing property), and even a narrow NSHostingController test would need to reproduce actual scene RESTORATION (saved window state from a prior launch), not just a fresh mount — the test shape that would catch a regression is a genuine relaunch-with-restored-state test (an XCUITest or a state-restoration test harness), not a lighter substitute.

G. Pane chrome: one pane menu, linked panes (ruled 2026-10-04)

Ruled by the maintainer, 2026-10-04. There is too much window chrome. As iTerm does, most per-pane controls move into one pane menu, opened from the pane head’s + or by right-clicking the pane head. It holds split, close, the pane-kind picker, the view mode, “What to show”, pin, and Link. Linking two panes (Link… in one, then the other) gives both the same light tint, and they follow each other’s zoom, position, page and magnifier, in page coordinates; unlinking is in the same menu. This settles the design of the maintainer’s earlier “drag to connect” proposal (panes.library.drag-to-connect, rulings of 2026-09-19). The breadcrumb shows only on a pinned pane, where it says something the window toolbar does not. This section also answers this spec’s open question “Sync granularity” (a link group).

Before and after, a Preview head (the maintainer’s example). Before, an unpinned Preview head reads [more] [close] [kind ▾] [↑ ‹ 1/2 ›] [paderov-mm10 › 00000013.jpg] [edit] [zoom] [pin] while the window toolbar already shows 00000013.jpg. After, it reads [kind icon] [↑ ‹ 1/2 ›] [pin] [+]:

Control today Where it is today After
more (overflow) PaneHead.swift controls capsule, folds when narrow retired: the pane menu is the one overflow
close ✕ identity capsule, PaneHead.swift:156-185 pane menu (Close Pane; ⌘W-style shortcut kept)
kind ▾ PaneKindSelector.swift:15 pane menu (Show In This Pane ▸); the head keeps only the kind’s icon as a label
↑ up to parent, ‹ 1/2 › page stepper PreviewHeadSelectorGroup, PreviewHeadControls.swift:14-84 stays on the head: it is navigation used constantly
breadcrumb breadcrumb capsule, PaneHead.swift:194-224 hidden unless the pane is pinned
edit (Edit Segments) PreviewHeadLensControls.swift:113-123 pane menu, inside What to show (where it already also sits, ReaderToolbar.swift:236-241)
zoom (zoom-controls toggle ⌘⌥E) PreviewHeadControls.swift:170-188 pane menu (View ▸ Zoom Controls; shortcut kept)
renditions ▲▼ and menu PreviewHeadControls.swift:139-217 pane menu (Rendition ▸); swipe up and down kept
pin PaneHead.swift:411-430 stays on the head, and is also in the pane menu
+ (split only today) PaneChromeMenu.swift:14 becomes the pane menu’s button
What to show (previewWhatToShow) the bottom quiet bar, ReaderToolbar.swift:217-256 pane menu (What to Show ▸)

Inventory of today’s per-pane chrome (2026-10-04, read from the code): PaneHead (Views/Shell/PaneHead/PaneHead.swift:28, bar height 40, PaneHeadMetrics :495-512) is the one head, used by Library (LibraryView+PaneHead.swift:54), Preview (ContentView+PreviewPaneHead.swift:86-153), Reader (ReadingPaneView.swift:465-523), Chat (ChatView+PaneChrome.swift:25), Segments (SegmentsPaneView.swift:134,160) and the workflow editor (WorkflowEditor.swift:141). Footers are not shared: Preview’s quiet bar (ReaderToolbar on MiniToolbar, image ZoomableImagePreviewMac+Overlays.swift:128, PDF PDFPageWithToolbar.swift:557-576, which repeats close, title and pin), the Library’s action bar (LibraryView+BottomActionBar.swift:43, its + adds an item), Chat’s (ChatView+PaneChrome.swift:57, its + is New Chat); the Reader has none. A right-click on the breadcrumb lists other libraries and the path (PaneHead.swift:213-243); only the Reader head has a whole-head context menu (ReadingPaneView.swift:507-521). Note: panes.vertical-no-breadcrumb above says the per-pane breadcrumb was retired; the code has one again in every head, and the window title and subtitle also show the path (ContentView+RootLayout.swift:311-317).

  • panes.chrome.one-pane-menu — [GAP] (#5435) every pane head has a + that opens the pane menu, and right-clicking anywhere on the pane head opens the same menu. Its entries, in order: Split Right, Split Below; Show In This Pane ▸ (the kind); View ▸ (the kind’s view mode and lens); What to Show ▸ (the layers, layers-on-the-source.md); Pin; Link… / Unlink; Close Pane. A kind that lacks an entry shows it disabled, never a different menu. Today the + offers split only (PaneChromeMenu.swift:14) and What to show sits in Preview’s footer.
  • panes.chrome.head-keeps-only-what-moves — [GAP] (#5435) a pane head keeps only the kind’s icon, the controls used constantly on that kind (Preview: up to parent and the page stepper), pin and +. Everything else in the table above moves into the pane menu, and the more/overflow capsule is retired. A footer that only repeats a head or menu entry (the PDF footer’s close, title and pin) is removed. Prerequisite for panes.chrome.head-and-footer-not-shared (#4880), since less chrome leaves one component to tune.
  • panes.chrome.breadcrumb-only-when-pinned — [GAP] (#5437) the breadcrumb shows only on a pinned pane; on an unpinned pane it repeats the window toolbar and is hidden.
  • panes.chrome.breadcrumb-opens-like-the-sidebar — [PARTIAL] (#5633; implemented with unit tests, not yet built or seen in the app) clicking a folder (or library) crumb in any pane head does exactly what clicking that node’s sidebar row does: it becomes the selection and the Preview shows its canvas, with the Library listing its contents. That holds even when the node is already the sidebar’s selection and the Preview is showing a file picked inside it: the crumb opens the folder again rather than doing nothing. One path: every crumb, and Preview’s up-to-parent step, goes through PaneCrumb.reveal → the sidebar’s reveal seam (SidebarView.openAsClicked), which writes the selection a click writes, or, when the node is already selected, re-runs the click’s own handlers (SidebarSelectionState.reopenGeneration). A relaunch restore’s reveal does not reopen, so it keeps the restored Library pick. Pinned by BreadcrumbOpensFolderTests.
  • panes.pin.in-the-pane-list — [GAP] (#5437) a pane’s pin is stored once, in the pane list (PaneScope.isPinned, Models/PaneList.swift:69, today never read), and read by every kind. Today pin is per-view @State in five places (LibraryPanePin, ContentView+Navigation.swift:402; PreviewPanePin, ContentView+DetailLayout.swift:666-690; a second, separate PDF pin, PDFPageWithToolbar.swift:69-74; the Reader, ReadingPaneView.swift:179-183; Chat).
  • panes.link.from-the-pane-menu — [GAP] (#5436, #4881) choosing Link… in one pane’s menu and then clicking another pane adds it to that pane’s link group; every pane in a group takes the group’s light tint (#4666), distinct from any other group’s; Unlink in a pane’s menu takes that pane out of its group. A group is stored in the pane list with its pane ids, so a saved workspace keeps it. Prerequisite: panes.model.per-pane-scope-and-kind-unread (#4887): PaneScope (PaneList.swift:54) is stored and every live read discards it (ContentView+PaneSpecs.swift:198,351).
  • panes.link.follow-in-page-coordinates — [GAP] (#5436, #4726) linked panes follow each other’s zoom, position and page, expressed in page coordinates (one kind of link: a group shares whatever its panes can, and a Library in the group shares its selection), so an image, a crop of it (one page of a spread, layers-on-the-source.md layers.spread.show-one-page), a translation of it and a canvas card of it stay on the same spot. Today no zoom, position or page sync exists; the image keeps position image-normalised (Models/PreviewImageGeometry.swift:18-20), PDF and Reader keep only a page number (PDFPageWithToolbar.swift:38, ReadingPaneView.swift:184,216), and the canvas camera is in world coordinates per scope (CanvasArrangement.swift:203-217).
  • panes.link.magnifier-follows — [GAP] (#5436, #4726) in linked panes the magnifier follows too: its point and its zoom in one pane show the same page spot in the other pane’s magnifier strip. Each pane keeps its own strip placement (at the side or the bottom, following the line’s direction, #5411). The instrument itself stays owned by preview-magnifier.md.
  • panes.link.colour-chip — [GAP] (#4881) linking is explicit, by colour: each pane head carries a small link chip showing its group’s colour (or none), and choosing a colour from it puts the pane in that group; panes with the same colour follow each other. A new split joins its parent’s group. The chip is the head’s one link control; Link… / Unlink in the pane menu set the same group. (Ruled 2026-10-04.)

Plan (each slice one reviewable commit with its tests):

# Slice Spec ids Work Pinned by Depends on
1 Pin in the pane list panes.pin.in-the-pane-list every kind reads PaneScope.isPinned; the PDF’s second pin removed Swift: pin round-trips through the list; one pin per pane #4887 (PaneScope read)
2 Breadcrumb only when pinned panes.chrome.breadcrumb-only-when-pinned PaneHead hides the breadcrumb capsule unless pinned Swift: unpinned head has no crumb, pinned has 1
3 The pane menu panes.chrome.one-pane-menu PaneChromeMenu becomes the full menu; the head’s right-click opens it; entries per kind Swift: every head mounts it, same order per kind; click-around: split and close from it —
4 Heads slimmed panes.chrome.head-keeps-only-what-moves, panes.chrome.head-floats-or-is-a-row, panes.chrome.footer-only-for-list-actions the moved controls leave the heads and the PDF footer; the overflow retires; What to show leaves the footer Swift guardrail: a head mounts only its listed controls; the a11y ids still reach the menu entries 3
5 Link and unlink panes.link.from-the-pane-menu a link group in the pane list; Link… pick mode; tint per group; selection shared within a group Swift: link stored and saved with a workspace; click-around: link, see tint, unlink 3, #4887
6 Follow in page coordinates panes.link.follow-in-page-coordinates one page-coordinate mapping per kind (image, crop, PDF, Reader page, canvas card); linked panes publish and apply zoom, position, page Swift pure: page point round-trips image ↔ crop ↔ canvas; click-around: zoom one, the other follows 5
7 Magnifier follows panes.link.magnifier-follows the magnifier’s point and zoom published in page coordinates; each pane draws its own strip placement Swift: the other strip shows the same page spot 6, #4725 (per-pane magnifier), #5411 (strip direction)

Ruled 2026-10-04 (the four pane-chrome questions; the maintainer took each recommendation):

  1. Where head controls live: over image-like panes (Preview, canvas) they float over the content and fade when the pointer is idle; on list and text panes (Library, Reader) they are a thin row (panes.chrome.head-floats-or-is-a-row).
  2. Links are groups: Link… adds a pane to the other pane’s group, and the whole group shares one tint (panes.link.from-the-pane-menu).
  3. One kind of link: a group shares whatever its panes can share: zoom, position, page, magnifier and selection (panes.link.follow-in-page-coordinates).
  4. Footers: a pane has a footer only where a list has its own actions (the Library’s add, Chat’s new chat), built on the one shared component; every other footer is removed (panes.chrome.footer-only-for-list-actions).

  5. panes.chrome.head-floats-or-is-a-row — [GAP] (#4974, #5435) on Preview and canvas panes the head’s remaining controls float over the content as a light overlay that fades when the pointer is idle and returns when it moves; on Library and Reader panes they are a thin row above the content. Today every head is a floating glass capsule bar 40 points high over a reserved row (PaneHead.swift, PaneHeadMetrics :495-512).

  6. panes.chrome.footer-only-for-list-actions — [GAP] (#5435, #4880) a footer appears only on a pane whose list has its own actions (Library: add; Chat: new chat), built on the one shared component; Preview’s quiet bar (image and PDF) and every other footer are removed, their entries living in the pane menu.

Known bugs to fix (already observed by the creative director)

  1. ~~Claims don’t reset on library change~~ — FIXED 5b709aca0 (F5): Claims + Entities now key their reload on the library and reset on a switch; pinned by ClaimsLibraryReloadKeyTests. Built + tested green.
  2. ~~Pin shares across split halves~~ — FIXED (F3, all pane kinds): pin is now per-split-half for Reader (was already), Preview (474802124), and Library (070cd1115). Preview/Library pin moved into per-sub-pane hosts (PreviewSplitPaneHost / LibrarySplitPaneHost) mirroring the Reader; a monotonic libraryPinClearToken carries the cross-cutting search-clear to the per-instance library pins. Pure seams (PreviewPanePin, LibraryPanePin); pinned by PreviewPanePinTests (6) + LibraryPanePinTests (7). Built + green.
  3. Preview isn’t always there (F6) — plan==render parity; close the #4525 gap.
  4. Chat prompt is under the image, not under the chat (panes.chat.below-sidebar) — move the chat history + input into the left sidebar beneath the folder tree.
  5. No two-of-a-kind panes (F7) — the enabler for 3 previews side-by-side; the pane-list generalization. (Larger, sequenced after the convergence decisions.)

Split-affects-both-columns and shared-zoom are in Verify-on-build above, not here — the code splits per focused slot and holds zoom per-instance, so those need a live repro first.

These are the first wave to pin — each needs a pinning test that asserts the behavior (not the code, per the capability-scrape ruling) and a fix that makes it pass. Delivering the Mail-style default (§Default composition) is the companion layout work, and falls out almost free once the pane list (F7) exists — a default is just a starting list.

Findings (code evidence, 2026-09-13)

Paths relative to fichero/fichero/. From a read-only code map; every line verified at the time of the audit. HISTORICAL: F1’s second renderer and the symbols it names (widescreenPaneRow, centerContentRouting’s raw switch) were deleted 2026-09-17 (#4683). The findings stand as the record of why the migration was needed, not as a description of the code now.

  • F1 — Two centre renderers. Views/Shell/ContentView/Layout/PaneSpec.swift:15-46,78-225 (PaneSpec, widescreenPaneSpecs, widescreenPaneRow) runs only in LayoutMode.widescreen; Views/Shell/ContentView/Layout/ContentView+SidebarLayout.swift:143-213 (centerContentRouting, a switch LayoutMode) is the legacy .standard/.none path. The .none branch there (170-180) is dead — an earlier !showsPreviewPane guard (line 154) diverts first.
  • F2 — Three pane vocabularies. Views/Shell/PaneVisibility.swift:5-7 (grid/canvas/reading) vs PaneSpec.swift:16-20 (library/preview/reading/chat) vs Views/Shell/WindowLayout/WindowWorkspace.swift:41-52 (showLibraryPane/…).
  • F3 — Pin scope split. (FIXED — Preview 474802124, Library 070cd1115: pin moved into per-sub-pane hosts PreviewSplitPaneHost/LibrarySplitPaneHost with pure seams PreviewPanePin/LibraryPanePin; library reset via a monotonic libraryPinClearToken.) Reader: per-instance @State inside the pane — Views/Reader/Page/ReadingPaneView.swift:131-135 (isPinned, pinnedDocument, …), read at :167-170, toggled at :544-555; the type doc (:6-9) states independence-per-split as the goal. Preview: @State var pinnedPreviewDocument on ContentView — Views/Shell/ContentView/ContentView.swift:60-62, toggle at ContentView+PreviewPaneHead.swift:116-119; each split half’s head reads the one shared property. Library: @State var pinnedLibrary on ContentView — ContentView.swift:63-64. Entities/Claims: no pin affordance.
  • F4 — Zoom is per-instance (no shared store). ReadingPaneView.swift:136 (@State var webZoom), Views/Preview/PDFViewer/PDFPageWithToolbar.swift:38 (@State var zoom = PDFZoomController()); SplittablePane.swift:516-521 invokes the content closure per slot → distinct @State. No AppStorage/SceneStorage/doc-id-keyed zoom store exists. The CD’s “split shares one zoom” is unexplained by the code — LIVE REPRO NEEDED before spec’ing a fix.
  • F5 — Claims not keyed to library. Entities: EntitiesLibraryContent.swift:64 (.task { store.loadEntities(…) }, no id:), store is per-library (Models/EntityStore.swift:46-47, injected LibraryWorkspaceRoot.swift:96). Claims: ClaimsLibraryContent.swift:65 (.task(id: folderId)), model cached @State … model ?? LibraryClaimsModel(service:) (:66), service captured permanently (LibraryClaimsModel.swift:29-33); library-wide row has folderId == nil both sides of a switch (LibraryView+ContentBranches.swift:215) so .task(id:) never refires. ContentView.handleLibraryChange() (ContentView+StateEvents.swift:316-331) clears detail/selection/search/KG-focus but neither KG table.
  • F6 — Plan claims preview content the view won’t render. PaneContentPlan.swift:82-162 vs ContentView+DetailLayout.swift:226-306 (previewView renders only .library; comment “the remaining #4525 step” at :298-300). Also .none gate (ContentView+ActionsClaim.swift:68-69) and collapse order (Models/LayoutMode.swift:186-207).
  • F7 — Fixed slots, not a pane list. WidescreenPanePlan = four Bools (Models/LayoutMode.swift:128-135); widescreenPaneSpecs appends one spec per flag (PaneSpec.swift:78-102); PaneSpec.Kind a fixed 4-case enum with an exhaustive kindContent switch (:173-225). A boolean can’t count past one → no two-of-a-kind. Split caps 2×2 (SplittablePane.swift:156-166) but every sub-pane renders the same content closure → identical-kind panes only. paneKindOverrides (ContentView.swift:65-67) can only trade the reading slot to a 2nd preview, not add a 3rd.

Test matrix (from the coverage audit)

The pane system’s pure-model layer is well-tested (PaneVisibilityTests, PaneContentPlanTests, WindowWorkspaceTests split-routing + snapshots, WorkspaceLayoutDefaultsTests, ContentViewPersistenceTests). The gaps are the exact reliability concerns — each is the first-wave pinning test:

Concern Today Required pinning test (behavior, not source-scrape)
Split focused-only isolation routing keys tested, isolation not splitting pane A does not change pane B’s split count
Pin under split (F3) 0 behavior tests (2 source-scrapes) pin left preview split-half; right half stays live; unpin releases — for Preview/Library like Reader
Zoom independence (F4) 0 tests zoom one split half; the other’s zoom is unchanged (after a live repro confirms the report)
Claims reset on library change (F5) MISSING (both tables) switch library → Claims (and Entities) show the new library’s rows, not stale
Plan==render (F6) plan tested, render-parity not for every mode the plan marks preview .content, the view renders content
N-pane / two-of-a-kind (F7) capped, untested the pane list can hold two .preview entries with different content
Convert the seed-write scrape WorkspaceLayoutDefaultsTests::testTheSeedAndTheWriteBackAreBothWired is a baselined scrape drive the view/store, toggle a pane, assert WorkspaceLayoutDefaults was written

Verify-on-build (before spec’ing a fix)

Two CD reports are NOT explained by the code as written; confirm empirically on a running build before treating them as bugs: - Shared zoom across a split (F4) — split a reader/preview, zoom one half, watch the other. - Split affects both columns — the code splits per focused slot in .widescreen; if the report reproduces, capture which LayoutMode and pane it happens in (likely the legacy path).

Design decisions — RATIFIED 2026-09-13 (creative director)

  1. Generalize to a pane list — YES, after the safe fixes. (F7) The plan becomes an ordered pane list instead of four Bools. A pane entry carries a kind AND a scope (which library / document / folder it shows), not just a kind — so the list can hold different previews, different readers, or even different LIBRARIES side by side (CD, 2026-09-13: “we want to be able to have different libraries, or different previews, or readers”), not merely N-of-one-kind. Enables 3-previews (original · words · reader), heterogeneous compositions, and saved workspaces; the Mail default becomes just a starting list. Sequenced after F3 (pin) + F6 (plan==render). Open sub-questions: the cap (max panes per row); whether 2×2 split stays once a list can add more of a kind; and how a pane’s scope is chosen/persisted (a per-pane library/document picker). Chat is NOT a row pane in this model — it lives inside the sidebar (panes.chat.below-sidebar).
  2. Zoom — independent + opt-in sync. (F4) Each split half zooms alone (matches the code); a “sync zoom” toggle links them on request. Still confirm the live repro of the “shared zoom” report first, in case there’s an actual bug to fix underneath.
  3. Retire the legacy renderer — YES, one renderer. (F1) Every layout mode routes through the pane-list path (.none/.standard = a shorter list); behavior can’t diverge by mode.
  4. Entities & Claims — YES, one view system. (F5) Shared data-lifecycle, selection, reset, pin — differing only in row content + open-target. F5 already aligned their reset.

Design decisions — RATIFIED 2026-09-14 (creative director)

Every layout is one workspace. There is no “bottom preview mode” vs “side preview mode” — those are the same nestable pane list drawn two ways, and the fact that one has pane heads (breadcrumb, close) and the other doesn’t is the F1 two-renderer bug, not a feature. The reliability north star lands as: one pane-list model, one renderer, and layouts are just saved instances of it. Worked examples the creative director wants expressible (all the same model, different nesting):

  • a table above a reader, beside a tall full-height preview;
  • three previews side by side;
  • a list at the top with a related-files list to its right, then three previews below it;
  • three previews across the top with one long-thin library beneath;
  • three previews and a reader, with a long-thin library.

  • Workspace Manager — a dialog. A simple manager to add, delete, and rename workspaces and assign each a keyboard shortcut, ⌘⌥1 through ⌘⌥9. Built-in starting workspaces (the Mail default, three-up preview, reader+preview) ship as ordinary entries the user can keep, edit, or delete — nothing is privileged. Pressing a bound shortcut switches the window’s pane list to that workspace. This is the surface on top of the F7 pane-list model (a workspace IS a PaneList, already Codable).

  • Workspaces double as the test fixtures (design-led testing). Each ratified workspace is a named PaneList fixture; the tests assert the spec’s workspace composes and renders as specified (kinds, order, split axis, pane heads present), never the code’s internals. Adding a workspace to the manager adds a fixture; a test that breaks means the renderer diverged from the spec, which is exactly the F1 divergence we’re closing.
  • Accessibility is first-class, not a follow-up. ⌘⌥1–9 workspace switching; full keyboard navigation between panes (focus ring moves pane→pane) and within the focused pane; every pane head (breadcrumb, close, kind switch) reachable and labelled for VoiceOver. A workspace the keyboard can’t drive is not done.

Existing machinery — inventory 2026-09-14 (what to keep, what to retire)

A read-only code map found the workspace feature is already LIVE, not a stub — so F7 is a consolidation, not a build-from-zero:

  • Catalog + persistence exist. WindowWorkspace.swift defines SavedWindowWorkspace (id·name·savedAt·WindowLayoutSnapshot) and WindowWorkspaceCatalog; WindowWorkspaceStore (.shared, @Observable) persists them to UserDefaults (window.workspaces). Save / apply / remove all work (ContentView+LayoutChooser.swift captureLayoutSnapshot / applyLayoutSnapshot).
  • The Workspaces toolbar icon is live. ContentView+Toolbar.swift:229 → workspacesMenu (ContentView+LayoutChooser.swift:90), SF Symbol rectangle.grid.1x2, a populated dropdown (Layouts · Split · Built-ins · Saved · Save Current… · Delete · Toolbar Buttons). A menu-bar twin exists (WindowLayoutCommands → WorkspaceCommandsSection).
  • TWO redundant built-in sets — RETIRED 2026-09-15/2026-09-18. BuiltInWorkspace (.reading/.cataloguing/.everything, with bars+toolbar) was deleted first; WindowLayoutPreset (.libraryOnly/.reading/.everything, visibility-only) and its WindowLayoutCommands.applyPreset verb were deleted 2026-09-18, confirmed callerless first — there is now ONE built-in catalog, BuiltInWorkspaceLayout (the five below). Enforced by WorkspaceSystemBoundaryTests. testNoParallelLayoutPresetSystemBesideWorkspaces.
  • The model split was the core debt — CLOSED 2026-09-18. Saved workspaces used to persist only a WindowLayoutSnapshot (six showXPane Bools + splits:[String:PaneSplitCounts] + paneKindOverrides) while the F7 model was PaneList (Models/PaneList.swift), NOT connected. WindowLayoutSnapshot.paneList: PaneList? connects them: capture = activePaneList, apply = assign it, so workspaces, claims, entities, reader and preview all render through the single path via the one field a saved arrangement was missing. See panes.workspace.save/.reopen and workspaces.persist-applied-list in §Behaviors D. The legacy paneKindOverrides WRITE side is now dead too (its live consumer, SplitCommandRouting, is deleted) — the Codable field stays, decode-only, for an old snapshot that has it.
  • Dead flags: ToolbarVisibilityPlan.showSplitMenu/showLayoutsMenu (decode-only) and LayoutMode.keyboardShortcut (unbound metadata) — delete on the way through.

The built-in workspaces — v2 SHIPPED 2026-09-16 (FIVE, ⌘⌥1–5)

The v1 built-ins (shipped 41880cde9) only show/hide panes. The CD’s brief (2026-09-15): make them real compositions — Mail-style and other genuinely useful arrangements grounded in who uses the app (an archivist browsing; a reader; a transcriber who wants the page image, its word boxes, and an editor at once; someone on a width-sensitive script who needs those stacked vertically; a collator). Each workspace is a PaneList. The sidebar (with chat beneath) is always present and is NOT a centre pane. H[…] is a horizontal row, V[…] a vertical stack.

SHIPPED (dfa937946, CD live 2026-09-16): the built-in set is FIVE, ⌘⌥1–5. BuiltInWorkspaceLayout enumerates exactly read · browse · transcribe · transcribeTall · compare. Catalogue and Claims were dropped (the cataloguer / KG-research personas), and there is no separate “Default Layout” — a fresh window seeds to Read (see the seed changelog below). Inspector is kept as an available add-on pane, not one of the five defaults. Pinned: Tests/Unit/general/Views/Shell/BuiltInWorkspaceLayoutTests.swift (allCases.count == 5; compare.defaultSlot == 5; Read composes exactly ONE library leaf; Transcribe·Tall is three-long).

# ⌘⌥ Name Composition Persona Built
1 1 Read [ library(docs,table) · reading ] beside preview(image) reader (default seed) [OK]
2 2 Browse H[ library(docs,icons) · preview(image) · reading ] archivist browsing [OK]
3 3 Transcribe V[ H[ preview(image) · reading ] · library(icons strip) ] transcriber [OK]
4 4 Transcribe · Tall V[ H[ preview(image) · preview(words) · reading/editor ] · library(icons strip) ] (three-long over strip) width-sensitive scripts [OK]
5 5 Compare V[ H[ preview(image)@A · preview(image)@B · reading ] · library(icons strip) ] collation [OK]
6 6 Library [ library ] alone browsing a project [GAP] (#4969)
7 7 Preview [ preview(image) ] alone looking at one page [GAP] (#4969)
8 8 Two Previews H[ preview(image) · preview(image) ] side by side comparing two pages [GAP] (#4969)
9 9 Library + Reader H[ library · reading ] reading through a project [GAP] (#4969)
  • panes.builtin.nine-defaults — [GAP] (#4969) four more built-in workspaces follow the five above, in this order: Library only, Preview only, Two Previews side by side, and Library + Reader, on ⌘⌥6–9. The pane icons stay as they are (#5027). (Ruled 2026-10-04.)

The removed Catalog / Knowledge / Everything workspaces (former #7–9) are not built. ⌘⌥6–9, formerly free for user-saved workspaces (the slot→workspace map), go to the four defaults above (ruled 2026-10-04, #4969). Their compositions above are retained only as design history for when the cataloguer / KG-research personas return.

Grounded in the surface inventory (2026-09-15): preview(words) = the preview pane with the OCR word-box overlay on (OCRGeometryOverlay, today a global imagePreview.inlineTextEnabled); library(claims|entities) = the library pane on the LibraryContentKind axis (sidebar-driven today); inspector(...) = the .inspector() sibling’s InspectorSection (source/notes/knowledge/artifacts); @A/@B = PaneScope.documentId pins (already in the model); V[…] = SplitAxis.vertical (already rendered).

Model changes the v2 workspaces need (the PaneList migration)

A pane leaf must carry per-pane configuration, not just kind + scope — this is the heart of the migration (CD-approved 2026-09-15):

  1. PaneKind.inspector (new) + a kindContent branch that hosts the existing inspector content as a centre pane (today it is a NavigationSplitView sibling).
  2. Library-pane config on the leaf: contentKind (documents / entities / claims — the existing LibraryContentKind axis) and displayMode (the existing LibraryLayout). So one leaf renders “library, claims, as a table.”
  3. Preview-pane config on the leaf: lens (preview / edit) and a wordBoxes overlay flag — making today’s global imagePreview.inlineTextEnabled per-pane, so Transcribe can show a plain image beside a word-box image.
  4. Saved workspaces become PaneList (replacing WindowLayoutSnapshot’s six Bools), applied by setting the window’s pane list. Collapse BuiltInWorkspace + WindowLayoutPreset into these nine.

Shortcuts: a slot→workspace map (CD-approved 2026-09-15) — the nine ⌘⌥ slots are one app-wide mapping the Manager edits; each slot points at exactly one workspace (built-in or saved); no conflicts by construction; the nine above are the default mapping. The Workspace Manager dialog (add / delete / rename / rebind slot) is built now, CD to verify visually.

F7 implementation plan — one renderer (2026-09-14)

Behavior-preserving increments, each build+unit-gated; the CD verifies each visually:

  1. Pure seam [done, then deleted 2026-09-17]. PaneList.forLayout(mode:showsPreview:showsDocumentGrid:widescreen:) was the bridge from the Bool-driven modes to a PaneList; once a workspace was always applied it had no production caller and went with the second renderer (#4683). Historical: reproduces centerContentRouting’s branch structure AS DATA; .standard bottom-preview becomes a split(.vertical,[library,preview]). Unit-tested in PaneListTests (mode→composition, the “same system” contract that preview is a pane in both side and bottom modes).
  2. One node renderer [done as paneListRow; widescreenPaneRow deleted 2026-09-17]. Original plan: generalize widescreenPaneRow into paneRow(_ list: PaneList) that renders a node recursively — leaf → kindContent (its head chrome + .clipped()); split → H/VStack of children along the axis with the existing ResizableDivider. Route centerContent through paneRow(PaneList.forLayout(...)) for the non-compact path; the compact reader flow is unchanged. Retire the centerContentRouting switch and its raw PlatformVSplitView. Result: bottom preview gains the breadcrumb/close head and the clip, and toggles act in every mode.
  3. Inspector as a PaneKind. Add .inspector so #6/#9 compose in the list rather than as a NavigationSplitView sibling.
  4. Workspaces on PaneList. SavedWindowWorkspace.layout gains/derives a PaneList; apply sets the window’s pane list; collapse BuiltInWorkspace+WindowLayoutPreset into the nine.
  5. Workspace Manager dialog + ⌘⌥1–9 (2026-09-14 ratified): add/delete/rename/bind, keyboard
  6. VoiceOver reachable.

Decisions needed to continue (2026-09-14) — for the CD

The one-renderer and the first six built-in workspaces (⌘⌥1–6) shipped; these block the rest:

  1. The ⌘⌥1–9 shortcut model (blocks saved-workspace rebinding + the Manager). Two shapes:
  2. (A) per-workspace shortcutNumber — each workspace optionally owns a number; the app resolves conflicts. Simple to store, but two workspaces can claim the same key.
  3. (B, recommended) a slot→workspace MAP — the nine ⌘⌥ slots are a single app-wide mapping the Manager edits; each slot points at exactly one workspace (built-in OR saved), so there are no conflicts by construction and the built-ins are just the default mapping. Today built-ins own ⌘⌥1–6 by list position (a special case of B). Recommend B.
  4. Workspaces #7–9 (Compare, Claims, Entities). Compare needs two preview panes with different scopes (only PaneList expresses that, not the six-Bool plan); Claims/Entities need the library pane’s view mode to be part of the workspace. Both imply moving saved workspaces onto PaneList + adding a per-pane view-mode/scope. Ruling needed on that model.
  5. The Manager dialog — data ops exist (save/remove/rename); the dialog UI is built once the CD can verify it visually (RenderPreview is toolchain-blocked in the agent environment).

Resolved 2026-09-15 (creative director)

  • Save = layout only. A saved workspace (and each built-in) captures the pane composition + per-pane config, NOT the live selection; panes re-fill from context. Pinning a specific document (Compare’s A/B) is an explicit per-pane opt-in (PaneScope.documentId).
  • Magnifier zoom = independent + a sync toggle. Each pane zooms alone; a “sync zoom” toggle links panes in a workspace on request (matches the code). Confirms the 2026-09-13 ruling.
  • Word-boxes default = recognised text inside each box (proof the transcription in place); a pane-head switch drops to outlines-only.
  • Claim/Knowledge subjects = the full set — people, places, organizations, events, concepts, citations, works, dates-as-subjects (supersedes the “richer subject types” open question).

RATIFIED 2026-09-15 — ONE system, enforced by tests (creative director)

“Don’t have two workspace systems, two rendering systems, etc. We want one system, well done.”

The pane/workspace feature shipped as a half-finished migration (spec §”Current architecture”): a legacy Bool-visibility system (BuiltInWorkspace presets, WindowLayoutPreset “Layouts”, WindowLayoutSnapshot, the showXPane toggles) running alongside the F7 PaneList model. Two systems for one job is the reliability root. This ruling closes it: there is exactly ONE of each, and a guardrail test keeps the second from growing back.

  • workspaces.one-system — [RATIFIED → enforced] A workspace is a PaneList — built-in or saved, there is one model. The built-in defaults are BuiltInWorkspaceLayout (FIVE v2 compositions as of 2026-09-16, ⌘⌥1–5 — Read · Browse · Transcribe · Transcribe·Tall · Compare; Catalogue + Claims dropped); user workspaces are saved PaneLists (⌘⌥6–9). The legacy BuiltInWorkspace enum and the WindowLayoutPreset “Layouts” presets are deleted, not hidden. Enforced: BuiltInWorkspaceSystemTests.noLegacyWorkspaceSystem — a source guardrail that greps the app target and fails if BuiltInWorkspace/WindowLayoutPreset/applyBuiltIn/ applyLayoutPreset reappear (their deletion is also compile-time). One list of defaults in the menus, never three competing lists.
  • panes.one-renderer — [RATIFIED → enforced, tightened 2026-09-17] The window centre is drawn by ONE path: paneListRow(activePaneList). activePaneList is non-optional — a workspace is always applied — so there is no fallback branch and nothing to fall back to. The earlier two-path shape (paneComposition for a derived default, paneListRow for an applied workspace) was deleted (#4683): the derived default had been unreachable since the workspace seed landed, and an unreachable second renderer is where drift hides. Deleted with it: PaneList.forLayout, WidescreenVisibility, widescreenPaneRow, paneDivider, paneContent(for:). Enforced: WorkspaceSystemBoundaryTests asserts the routing file contains paneListRow(activePaneList) and contains neither paneComposition( nor PaneList.forLayout(, and asserts activePaneList is declared non-optional — so reintroducing either the second renderer or the Optional that kept it alive fails a test by name. Residue RESOLVED 2026-09-18: menu Split now routes through activePaneList.splittingLeaf (#4685; widescreenPaneSpecs/SplitCommandRouting deleted, confirmed callerless first) and the three legacy visibility Bools are DELETED, not merely bypassed (#4687; paneVisibility derives from activePaneList.kinds, so there is nothing left for toolbar/menu state to drift from). One renderer, one model — see the 2026-09-18 changelog entry above for the full list.
  • panes.instance-safe stays enforced by everyBuiltInIsInstanceSafe (below) — one system does not mean one pane; two same-kind panes are fine, and the structural guard keeps them loop-free.

Migration order (each increment built + committed before the next): (1) instance-safety fix; (2) delete the legacy built-in/preset lists + wire ⌘⌥1–6 to BuiltInWorkspaceLayout; (3) saved workspaces store a PaneList, the window is always a PaneList, the Bool-visibility path retired; (4) split/close act on the focused leaf instance (panes.split.focused-only / panes.close.this-pane-only). Tests grow with each step and cite the behavior id they pin.

Increment 3 — RATIFIED 2026-09-15 (evening, CD): PaneList is the ONE split model

“We might want 3 columns, one long below — or even four columns and 2 below. And remember: one system, not multiple systems.”

  • workspaces.pane-list-is-the-split-model — [RATIFIED] the window is ALWAYS a PaneList, and ALL splits — including the rich compositions the CD wants (3 columns over one wide pane; 4 columns over 2) — are expressed by PaneList’s NESTED splits, e.g. 3-cols-over-1-wide = split(.vertical, [ split(.horizontal, [a, b, c]), d ]); 4-over-2 = split(.vertical, [ split(.horizontal, [a,b,c,d]), split(.horizontal, [e,f]) ]). Nesting already expresses everything SplittablePane’s in-slot 3-per-axis / 2×2 grid did, and more, so the head “+” and the window Split commands must build these into the activePaneList (splittingLeaf + friends), NOT the leaf’s own SplittablePane. The SplittablePane split mechanism is then RETIRED — its 3-per-axis/grid model is the “second system” this consolidation removes. One model, one place splits live, and a workspace saves exactly what you see.
  • Consequence: PaneNode may need equal-flex weights (or explicit ratios) so “one long below” reads as a greedy row under three short columns — the height policy verticallyFramedPane fakes today becomes per-node data on the PaneList. Split/close then act on the focused leaf uniformly (close already does, via \.paneCloseAction → removingLeaf; 2026-09-18: the window Split commands now do too, via activePaneList.leafIDs(of: kind).first → splittingLeaf — a plain function call resolving focusedPane ?? paneFocusHint, not a per-leaf environment seam like \.paneCloseAction’s, since the menu commands aren’t inside a specific leaf’s own view subtree the way the pane head’s close button is. See panes.split.focused-only for the Compare instance-precision caveat this leaves open).

Pane-linkage color coding — IDEA 2026-09-15 (CD), for design

  • panes.linkage-color — [GAP/IDEA] (#4666) In a multi-column workspace (Compare especially) it isn’t obvious which library/list drives which preview/reader. Idea: tint each linked pane GROUP with a soft background color — the Xcode-theme-picker model (colored row bands) — so “this list → this preview → this reader” reads at a glance. Each PaneScope group (a column’s library + the panes that follow its selection) shares one tint; unlinked/independent panes stay neutral. Needs a design pass: how tints are assigned (per column? per scope link?), how quiet they stay (Golden-Gate restraint — a wash, not a highlight), and dark-mode behavior. Captured from the CD’s Xcode theme-picker reference. (Designed by the 2026-10-04 ruling on #4881: panes.link.colour-chip.)

Rulings

  • 2026-10-04 (#4977): a split opens the complementary pane (a Library opens a Preview, and on along the Library → Preview → Reader chain), except a Preview, which splits to another Preview (panes.split.opens-the-complement).
  • 2026-10-04 (#4969): four default workspaces are added after the existing five (Read, Browse, Transcribe, Transcribe Horizontal, Compare): Library only, Preview only, Two Previews side by side, and Library + Reader, on ⌘⌥6–9; the pane icons stay (#5027) (panes.builtin.nine-defaults).
  • 2026-10-04 (#4881): pane linking is explicit, by colour chips on the pane heads; panes with the same colour follow each other, and a new split joins its parent’s group (panes.link.colour-chip).
  • 2026-10-04 (#4886): any Entities pane drives the Claims pane directly below it; with no entity selected, the Claims pane shows every claim in its scope. Owned by kg/kg-tables.md (kg.tables.entities-master-claims-detail).

Open questions (for the design lead)

  • Naming. What do we call editing directly inline within a pane, where each pane may carry different settings? (“inline edit mode” / “live edit” / something else?)
  • Richer subject types. Claims today anchor people / places / organizations (and the model already has event / concept / citation / other). Should the claims tables and subject pickers surface the full set — and are there types beyond the current enum the creative director wants (objects, works, dates-as-subjects)? (Ties kg-interactions claim-richness.)
  • Answered (this spec section G panes.link.*; maintainer 2026-10-04: pane linking by colour chips): A link group shown by colour chips. Sync granularity. (ANSWERED 2026-10-04 in section G: a link group, panes.link.*.) Is magnifier sync a per-window toggle, a per-pair binding, or a per-pane opt-in?
  • Workspace scope. Does a saved workspace capture a live selection (these four people) or only the pane layout, rehydrating selection from context?

CD runtime review 2026-09-15 (live build) — split/close not pane-scoped

Creative director, running the app (the one-renderer + old split/close wiring still in place):

  • panes.split.focused-only — [FIXED, model + isolation] Splitting one pane splits ONLY that pane (pinned by PaneListTests “splitting a pane splits ONLY that pane”; the live per-slot @SceneStorage isolation landed 2026-08-24). RESOLVED 2026-09-18 (was “Open — increment 3 design question” below): the toolbar/menu-bar Split commands now route through activePaneList.splittingLeaf(id, axis:), not the leaf’s own SplittablePane mechanism — a split made this way IS stored in the PaneList and DOES save with the workspace. See the canonical entry in §Behaviors A (above) for the exact routing and the Compare instance-precision caveat that remains open.
  • panes.close.this-pane-only — [FIXED 2026-09-15, applied path] Closing a pane in an applied workspace now removes THAT leaf from the stored PaneList (removingLeaf(id), which collapses a singleton split to its survivor and removes a top-level pane in one operation) — never the whole row. Wired through a \.paneCloseAction environment seam that PaneHead’s X prefers over the legacy scope-shared close (commit ba575a64e; model pinned by PaneListTests). UPDATE 2026-09-16 (dfa937946): the always-a-PaneList step landed — activePaneList seeds to Read, so the window is always the stored PaneList and the legacy visibility-Bool path no longer renders. Split-then- close-both is also fixed (the PaneHead close-ladder now collapses an active in-slot split by one before removing the whole leaf). UPDATE 2026-09-18: the split side is now symmetric with close (see panes.split.focused-only above) — both act on the stored PaneList by leaf id.
  • panes.head.consistent-minimal — [PARTIAL] (implemented, unpinned; #4796) (fixed 2026-09-16, dfa937946) every pane head is now the same consistent liquid-glass style with no per-kind chrome: PaneFilterBar.showsSeparator defaults OFF (no Library/Reader hairline), and the ChatView standalone Divider was removed. \.isSolePane additionally collapses the head’s close control when a pane is the only one. (The preview’s minimal, line-less, tight style is now the shared one — Golden-Gate restraint.) No test found pinning showsSeparator’s default or isSolePane’s effect — tracked in #4796.
  • panes.inspector.icon-tabs-and-empty-state — [GAP] (#1854) the right-inspector’s own tab bar should be compact SF-Symbol icon tabs (Xcode-style), and an inspector with nothing selected should show a centered “No Selection” placeholder rather than blank or stale content. Verified NOT built: DocumentInspector+TabBar.swift:70 renders Text(facet.rawValue) — text tabs, not icon tabs — and no “No Selection”/empty-state view was found anywhere under Views/Inspector/. Distinct from panes.head.consistent-minimal above, which covers chrome CONSISTENCY (no per-kind hairlines/dividers) — this behavior is about the tab bar’s own presentation and the empty state specifically, neither touched by that fix.
  • panes.head.drag-to-rearrange — [GAP, requested] (#4748) Dragging a pane by the icon at the LEFT of its head (the kind/preview icon) should let the user move that pane elsewhere in the composition (reorder / re-nest). A direct-manipulation complement to the pane list. New.
  • panes.head.inspector-always-visible — [GAP] (#1199) an inspector column should be a stable, always-present rightmost pane across every view (library, reading, KG graph, workflow), never disappearing so a user loses their place. Verified NOT true today: ShellLayoutPolicy.swift takes an inspectorVisible: Bool and a collapseInspector policy that together hide the inspector below a width threshold (:80-127) — the inspector is conditionally hidden by design, not a bug that slipped through. Open design tension, not decided here: under the current “one system, every pane is a PaneList leaf” architecture (workspaces.one-system above), the inspector is itself just one pane kind among many — a user can already close or rearrange it like any other pane. Whether “always visible, never hidden” survives that architecture, or whether the issue’s 2026-06-08 framing predates it and needs re-scoping to “never auto-collapsed below a size threshold” specifically, is a real open question — reshaped, needs maintainer triage, not decided by this pass.
  • panes.head.names-its-own-window — [PARTIAL] (fixed 82ae96b9b; #4860 stays open for the maintainer to confirm on screen) a pane head names the library ITS OWN window (or, once panes carry their own scope, its own pane) is showing — never an app-wide pointer that any window could have last written. Was BROKEN: the root crumb and drag payload were built from LibraryManager.shared.currentLibraryId, the app-wide manager’s own property, so with two window tabs on different libraries the crumb showed whichever window wrote last — the same singleton-pointer mistake kg.entity.focus-is-per-window (kg-entity-inspector.md) fixed for entity focus. Fixed: LibraryView+PaneHead.swift’s root crumb and drag payload now read the WINDOW’s own library. Found in the same audit, since resolved (2026-09-26): the same app-wide-pointer mistake in the workflow editor’s crumbs, the Reader’s crumb drag payload and new-window paths, the artifact-lens sites, the claim-card and PDF-toolbar sites and the unused helper. No view under Views/ reads the app-wide pointer now. A deliberate app-level use of currentLibraryId (which library File > New targets) stays explicit and out of scope, as before. Pinned tree-wide, a STRONGER pin than the per-site tests it replaced (the earlier per-site suites checked ten named sites and were deleted): LibraryPointerGuardrailTests.noViewReadsTheAppWideLibraryPointer (file fichero/Tests/Unit/general/Views/Shell/LibraryPointerGuardrailTests.swift) scans every file under Views/ for LibraryManager.shared.currentLibraryId, so a NEW view reading it fails too. Not pinned: the positive half, that each site reads its own windowState.libraryId.

  • panes.instance-safe — [FIXED 2026-09-15] a workspace may mount more than one pane of the same kind in one window (Compare: two previews, two readers, two libraries). Applying it used to beachball: repeated makeNSView, “Fetched 8 artifacts / Loaded 0 annotations” repeating, a 32-second sidebar→content update. Root cause (confirmed, not the earlier selection/shared-state hypothesis): a window-scoped focusedSceneValue admits exactly ONE publisher per key. Each pane-list leaf mounts its own unsplit SplittablePane, so two same-kind leaves both have isSecondarySplitPane == false and both publish the SAME scene keys (\.librarySelectAll, \.readerLens, \.imageZoomActions, …) every frame → SwiftUI’s “FocusedValue update tried to update multiple times per frame” fault → recursive scene-graph invalidation → the remount storm. It’s the exact fault the intra-SplittablePane library split already fixed via \.isSecondarySplitPane (LibraryView+KeyboardShortcuts, applyFocusedActions); the applied-workspace path defeated that guard because each leaf is its own pane, never marked secondary. Fix: PaneList.secondaryLeafIDs() flags every leaf whose kind already appeared earlier in traversal order; PaneSpec.paneNodeView injects .environment(\.isSecondarySplitPane, true) on those, and the reader / image-preview / image-editor publishers now gate their focusedSceneValue on the flag (library already did). Only the PRIMARY of each kind publishes; the duplicates render content only. Compare ships in its full designed form again. Completion (88ad63eea): a duplicate leaf mounts its own unsplit SplittablePane, whose primary re-published isSecondarySplitPane = false and defeated the flag — so SplittablePane now ORs an inherited-secondary flag into every splitPane (isSecondary || inheritedSecondary), keeping a duplicate leaf’s whole subtree secondary. Without this the flag never reached the publishers and the loop returned. Runtime-confirm with a ⌘R on Compare (env propagation isn’t unit-testable). Layout-recursion crash also fixed (2026-09-16, dfa937946): WorkspaceSplitStack stacked a frame(width: fixed) + frame(maxHeight: .infinity), feeding an unbounded proposal into nested AppKit panes → infinite _layoutSubtree recursion. Rewritten with GeometryReader + clamp + a single per-child frame. The Transcribe/Compare library strip is pinned to 72pt via PaneConfig.paneExtent while content flexes. Pinned by PaneInstanceIndependenceTests + BuiltInWorkspaceLayoutTests (film-strip pin).

Testing this class (design-led answer, “why can a test do these”): the fault is a Scene-level focused-value collision, and where it lives dictates the test: 1. Structural DATA guard (the guardrail — have it now): the loop is deterministic from the PaneList shape, so a pure unit test asserts instance-safety at the model level — BuiltInWorkspaceLayoutTests.everyBuiltInIsInstanceSafe: among the leaves NOT flagged secondary (the publishers), each kind appears at most once. A default that would loop fails here before it can ever render. compareFlagsDuplicatesSecondary pins that Compare marks one of each kind secondary. Fast, deterministic, and immune to the arm64e canvas bug. 2. XCUITest apply-workspace-and-stay-responsive (the only runtime reproduction): apply each workspace against a real WindowGroup Scene, then assert a follow-up interaction completes within N seconds (a beachball fails it). Belongs in the click-around leg. NOT ImageRenderer/#Preview (corrected): a snapshot render has no Scene and no @FocusedValue environment, so it CANNOT reproduce a “multiple updates per frame” fault — a render-with-timeout would pass green while the app hangs. Snapshots are the wrong tool for this class; the structural guard is the right one.

These are per-instance-state defects (spec §”Per-instance pane state for every kind”, F3): split count and close must key on the pane’s own slot, resolved from focusedPane, not a window- or row-shared coordinator. Fix design-led: add the failing pinning test (splitting pane A leaves pane B’s split count unchanged; closing pane A leaves the row intact) BEFORE the fix. The wiring of the six workspaces (apply + ⌘⌥1–6 + Manager) rides on the same per-instance renderer, so this is fixed first.

MCP-controllable layout — RATIFIED 2026-09-15 (CD)

The pane layout must be adjustable from MCP, not just the SwiftUI UI — because a workspace IS a PaneList (Codable), the same data an agent would send (spec [[agent-chat-model-is-a-user]], [[one-audited-action-layer]]). So the window’s current pane list lives in an observable, settable store (one endpoint the UI mutates), and MCP exposes typed tools over it: - apply a named built-in workspace (BuiltInWorkspaceLayout) or a raw PaneList to a window; - split / close / reorder a specific pane (by slot) — the same per-instance operations the UI does; - read the current pane list back (so an agent can see the composition it’s arranging). This makes the layout scriptable and testable end-to-end (the cross-surface invariant: UI == MCP), and is why the per-instance split/close fix matters for BOTH surfaces at once.

Preview harness

WorkspaceLayoutPreview (fichero/fichero/Views/Shell/WindowLayout/WorkspaceLayoutPreview.swift, #if DEBUG) renders every BuiltInWorkspaceLayout as a labelled mini window from the real PaneList data — the fast, no-boot canvas surface for verifying and screenshotting the built-in compositions (spec template §”Preview harness”). Its #Preview “Workspaces — the defaults” is the source for the manuals’ workspace screenshots. (As of 2026-09-16 the built-in set is FIVE, ⌘⌥1–5.)

Cross-references

  • kg-entity-inspector.md — the entity pane’s statements → source model.
  • kg-tables.md / kg-interactions.md — the entities/claims tables this view system hosts; claim richness and the shared interaction verb set.
  • segment-representations.md — the words/transcription representation that panes.words.fill-bounding-box renders into segment geometry.

Triaged from the backlog (2026-10-04)

  • inspector.tabs-fit-default-width — [GAP] (#4540) the document inspector has fewer top-level tabs (fold citations+related, or kg into entities) so it sits at its default width; Preview/Reader/Inspector stay three surfaces.
  • shell.no-nstableview-reentrancy — [GAP] (#4684) launch logs no “reentrant operation in its NSTableView delegate” warning (SwiftUI List; becomes an assert on a future macOS).
  • panes.location-bar-canonical — [GAP] (#3405) The top location bar is the one clickable breadcrumb, shows human page names (never import filenames), and is Liquid Glass styled.
  • panes.focus.active-pane-ring — [GAP] (#3406) focusing a split pane briefly shows a focus ring and the top location bar and the sidebar-selection target follow the active pane, with no backend churn.
  • panes.builtin.every-workspace-renders-in-ci — [GAP] (#4670) a test renders each built-in workspace at a small fixed size and fails on a cyclic or unsatisfiable layout, proven by a deliberately cyclic fixture.
  • shell.no-appkit-progressview-layout-fault — [GAP] (#3354, #3382) launch and library use log no ‘AppKitProgressView maximum length … min <= max’ fault; no indeterminate ProgressView sits in a fixed-size frame (SidebarRowLabelCore.swift:105-151 still does), guarded by ProgressViewUsageTests.
  • panes.selection.one-vocabulary-across-surfaces — [GAP] (#4563) the toolbar island, sidebar and bottom bar agree on the selection (no singular ‘Page 1’ for a multi-selection) and the Library shows a sidebar multi-selection as selected; the sidebar platter half is built.
  • shell.accessibility-labels — [GAP] (#3691) every control and custom view has a meaningful accessibilityLabel, correct traits and grouping; icon-only buttons are labelled.
  • shell.dynamic-type — [GAP] (#3694) text scales with the system Larger Text setting using semantic styles, composing with the reader font-size override, without breaking layouts.
  • shell.system-accessibility-settings — [GAP] (#3695) Reduce Motion and Increase Contrast are honoured (only ImmersiveReaderView and SkeletonPlaceholder read reduce-motion today).
  • panes.empty-state.names-itself-first — [GAP] (#5273) every empty pane shows three lines, the pane’s own name, then the state, then what to do (Reader / No Selection / Select a page in the Library to read it), never a bare ‘No Selection’.
  • panes.island.path-of-icons-last-is-the-proxy — [GAP] (#5274) the document island is a path of icon-plus-name segments with chevrons that collapses to an ellipsis and expands like Xcode’s jump bar, and its last icon is the selection’s draggable proxy (a count badge for several).
  • panes.split-stack-scene-key-changes-only-with-the-pane-list — [BROKEN] (#4994) WorkspaceSplitStack’s SceneStorage key changes only when the pane list changes, so SwiftUI’s ‘may not change its key’ warning does not flood the console.
  • panes.no-multi-second-main-thread-stalls — [GAP] (#4997) pane and workspace changes never stall the main thread for seconds in SwiftUI graph updates (ContentView.mainContent, ObservationCenter.invalidate), measured by the stall sampler.
  • panes.library-strip-scrolls-along-its-shape-only — [GAP] (#5000) a bottom Library film strip scrolls horizontally only and a narrow side Library strip vertically only, following the pane’s shape.
  • panes.split.halves-open-equal-inside-their-slot [BROKEN] (#5011): splitting a pane opens two equal halves, at any depth of nesting, and no pane’s head, body or footer ever draws outside the slot its parent gives it. (The gap half was fixed in fa4f49ef7.)
  • panes.new-tab.sidebar-keeps-its-width [BROKEN] (#5013): a new tab opens with the sidebar at the width the window already uses, not about twice as wide.
  • panes.visibility.show-brings-back-the-same-pane [BROKEN] (#5027): hiding a pane and showing it again brings back the same pane, in its old place, at its old size, with its own kind and view, not a new pane at the end of the row.
  • panes.head.kind-name-comes-from-the-kind [GAP] (#5062): a pane head’s kind chooser always shows the name of the kind the pane really is, because the name comes from the kind and is never passed separately.
  • panes.inspector.sidebar-material [GAP] (#5226): the Inspector column (and its tab strip) sits on the same translucent system material as the sidebar, as Xcode’s inspector and NetNewsWire do, so both side columns look like one family in light and dark mode with text contrast intact.
  • panes.click-answers-in-its-own-frame [GAP] (#5253): a click in the Reader, the Inspector, the Segments pane, a Preview page turn or a pane-kind switch changes the clicked thing in the same frame; whatever follows (another pane, an image, an engine call) comes after, with a visible loading state if it takes more than a moment. Each seam is timed in InteractionProfile. (The Library half landed in 12c9c23ef, 261485d4c.)
  • panes.sidebar.title-bar-holds-sidebar-chat-and-add [GAP] (#5292): the sidebar’s title-bar area holds, left to right, Show/Hide Sidebar fixed at the far left, Show/Hide Chat beside it, and a large + pull-down carrying New Folder and Import ▸ Link, Copy, Move Files…; when the sidebar hides, Chat and + go with it and the sidebar button stays put. (Orders the chat button right of the sidebar button, unlike panes.chat.toggle-in-sidebar-top.)
  • panes.head.kind-picker-matches-the-view-menu [GAP] (#5297): the pane head’s view picker lists Icons, List, Table and Columns as the View menu now does (ruled 2026-09-30 and built there in 1762871ce): each with its icon and a native checkmark, in shortcut order.

Future (ideas, not scheduled)

  • (#1640) Library and reading-surface toolbars compress when narrow like the inspector mini-toolbar (Reader uses ViewThatFits; Library does not)
  • (#1693) See several selected items at once (tabbed or side-by-side detail)
  • (#1839) AppKit fidelity pass on document inspector and reading-surface list
  • (#1841) Right-clicked row rings as menu target without changing selection (Mac polish)
  • (#1842) Drag-session visibility: dim dragged row and recover on out-of-window drop (Mac polish)
  • (#1929) Liquid Glass design-idiom audit across chrome
  • (#4045) Detach chat into its own window and save/restore a workspace window’s zone layout.
  • (#4168) Extend drag-out/drop-in (real file + RTF + text) to every object type: entities, claims, artifacts, notes, workflows, results.
  • (#4170) Draggable proxy icons in every window title bar and breadcrumb, with Finder-style path menu.
  • (#2320) visionOS port epic; no visionOS destination exists (SUPPORTED_PLATFORMS is iphoneos/macosx only); product vision.
  • (#2321) Add visionOS destination to the target; part of the visionOS vision, not scheduled.
  • (#2322) visionOS compile-clean pass; depends on the unscheduled visionOS target.
  • (#2323) visionOS remote-only engine + pairing reuse of iOS path; future.
  • (#2324) visionOS spatial reading surface (windows, ornaments); future.
  • (#2325) Volumetric KG in a RealityKit volume on visionOS; future.
  • (#2398) ARKit/immersive Spaces view placing documents on walls and floor; vision.
  • (#2426) Pin a document preview to a wall in ARKit (Vision Pro, iPhone); vision.