# The GUI arc — framing **Status: revision 3 — APPROVED 2026-08-10.** Approved on its design; the seven accuracy and process edits requested with the approval are folded in, and no further review round is required before the Stage 0 branch. **This document is itself the framing for Stage 0**, which is docs-only. Revision 3's opening previously said that *every* stage gets its own framing while §10 proceeded straight from this document into Stage 0 — the two could not both be true. The rule, stated exactly: - **Stage 0 is framed by this document.** No separate framing round; it ships documentation only, and its scope is enumerated in §5. - **Stages 1 through 10 each require their own framing**, approved before that stage's branch, as the arc-level contract-ownership rule demands. This document owns ordering, gates and the arc-level bar — never a stage's acceptance criteria. **Revision 3 answers the second review round (two blocking findings, seven required corrections, and nine rulings that close Q#GA4–Q#GA12).** - **The closure comparison is falsifiable now** (blocking №1). Revision 2's ordinal ordered grades but never said how to *assign* one, and "normalize to the head grade" silently mis-graded compound rows: a step reading "Works but undiscoverable" took `Works` from its first word while the failing half became a non-blocking annotation — even though **discoverability is the substance of steps 4, 7 and 11**, not a qualifier on them. §3.3 now defines observable criteria for each grade, grades a compound step by its **weakest required subclaim**, and separates **local TUI / attached TUI / GPU** into three columns. The comparison is GPU against the **local TUI** — the canonical `pmacs .` journey — with the attached TUI retained as evidence, because it is what distinguishes a semantic-wire gap from a frontend-local one. - **Half B's dependency graph was inverted, and is re-ordered** (blocking №2). Viewport facts cannot be designed before the multi-window model decides whether semantic windows are daemon projections or frontend-local objects, because that decision determines **the identity a viewport fact is *about***. And a framing-only stage cannot hand the sidebar geometry it consumes. Half B is now 6 model framing → 7 viewport/window-identity substrate (with smooth scroll and the scrollbar) → 8 splits **plus implemented** non-bottom side geometry → 9 sidebar riding Stage 8 → 10 tabs. - **"The GPU consumes daemon `view_top`" was backwards.** The backlog says the opposite in as many words — "the GPU **never** consumes daemon `view_top`" (`docs/side-quest-backlog.md:147`). Corrected. - **Stage 4b owns save *and* restore as a pair.** Q#DS9 scopes both to local mode and makes **both** no-ops under a daemon, enforced in Rust (`desktop-save-framing.md:222`). Revision 2's Q#GA10 claimed only the restore *trigger* remained open; snapshot ownership, save timing and multi-frontend arbitration remain open too. - **The Bell audit was wrong.** The daemon already emits `InstanceSignal::Bell` (`src/daemon.rs:1373`), at the audit anchor `4bc55e8` as well; `src/frontend.rs:349` consumes it and `pmacs-gpu` does not. Bell is **consumer-only** work for Stage 1c, with no producer question to answer. - **Stage 0 no longer contradicts the portability rule.** Revision 2 put the absorption PR *before* committing this framing, which leaves the approved framing living only in one worktree. The approved framing and the Stage 0 lane are now the branch's **first commit**; if synchronization stays a separate PR, that PR carries the framing first. - **No orphan scorecard row.** A GUI-product grade needs criteria and ground truth under `COHERENCE.md` §16 — a distinct **product** subgrade beside the architectural one — before the scorecard can point at it. - **§2.5 stops overclaiming.** It said the arc sequences the whole GPU backlog while items had neither stage nor deferral. Every item is now mapped or explicitly left standing, in a table. - **Q#GA8's temporary island is withdrawn** — ruled against, so this arc adds **no** off-path hardcode at all (§7). - **Reconnect attribution covers the silent cases** (§7). EOF and crash may deliver no `Goodbye` at all; "the daemon's stated reason" alone would have been unsatisfiable exactly when it matters. **One correction this round found on its own**, not raised in review: §3's condition 1 listed the Q#GA3 goals as "Stages 5, 6 and 10" while §5 marked **four** stages as Q#GA3 goals — the sidebar was missing from the closure sentence. Fixed, and re-checked against the renumbering. **Origin.** A daily-driver report, 2026-08-09: *the TUI is a suitable daily driver; the GUI feels behind similar editors, enough that the reporter would default to a different editor before using pmacs' GUI.* This is the same shape that opened the QoL arc (one daily-driver report → an arc that closed when the report's complaints were answered on both frontends), at a larger scale — so it gets a standard and an arc-level frame before any lane, not a framing per gap. **Ground truth below was established 2026-08-09** by a three-lane audit (GPU frontend inventory, TUI/grid inventory, documentation sweep) at `main` @ `4bc55e8`, with the §2.2 producer matrix re-verified against `src/semantic_render.rs` at the same anchor. Per `COHERENCE.md`'s citation convention: symbols first, `file:line` second; line numbers drift, symbols are authoritative. Three arc-shaping questions were put to the user and ruled on 2026-08-09; they are recorded as resolved, not open: - **Q#GA1 — RESOLVED: one arc, two halves.** Half A is maturity (input, parity, chrome, robustness, hover); Half B is structure (viewport facts, splits/multi-window, side surfaces, and the presentation stages that depend on them). The arc closes only when **both** halves land (§3). - **Q#GA2 — RESOLVED: the closure bar is journey parity plus an empty blocker list**, now stated as a conjunction over the whole stage map (§3). - **Q#GA3 — RESOLVED: all four GUI-native affordances are goals, none is a non-goal.** Hover/signature popups, smooth scroll + scrollbar, and a project/files sidebar are in-scope goals. **Tabs/tabline is a declared goal at deliberately low priority, sequenced last** — the user's ruling verbatim: it is not a non-goal and "would be nice down the line," with care required. What the care means is Q#GA12 plus the anti-patterns pinned at Stage 10. Coherence sections this framing serves: §2 (the journey, which Stage 0 makes frontend-graded), §3 (the recommended default surface for the graphical frontend), §6 (interaction islands — see §7's accounting), **§7 (first-class workspaces — two stages are gated on it, §5.1)**, §14 (workbench primitives — the sidebar is tree-primitive adoption), §16 (the semantic-frontend grade this arc completes the product half of), §20 (priority order — placement is Q#GA5). --- ## 1. Why an arc: the diagnosis **The GPU frontend is a rendering showcase over a single buffer, not a workbench.** It is genuinely ahead of the TUI on rendering richness — a real minibuffer candidate dropdown where the grid has only an inline `[candidate]` suffix, a minimap, inline math, pixel-precise squiggles, correct grapheme shaping where the grid drops combining marks in body text — and behind on the three things that make an editor a daily driver. Those three are the arc's spine: 1. **The one-window ceiling.** `State` holds exactly one `buffer` and one `current_buffer_id`; the daemon's entire per-frontend split layout (`Layout::compute`, `core.views`) is invisible to a semantic session. The bottom panel band (`PanelBand`) is a hand-built special case of "a second region." Everything spatial queues behind the general version: splits, side windows beyond `Side::Bottom`, the project/files surface `COHERENCE.md` §3 names, per-window status bands. The July roadmap called this "the largest unscoped design problem" and it still is. 2. **The GUI cannot be driven like a GUI.** `keymap_stack::Scope` has no frontend identity and `FrontendEvent` has no command-invocation variant, so a GPU-only binding cannot exist (the reason #220 shipped zoom as unbound commands — handoff §6's capability-aware keymap item). Beneath that, `translate_key` and the winit handler consume a narrow slice of desktop input; the rest lands in `_ => {}` (§2.3). 3. **The monolith has no test seam for input.** `pmacs-gpu/src/main.rs` is ~11.7k production lines in one file; the `gpu-terminal-input` framing already recorded that GPU key routing is untestable because `App::window_event`'s logic is inline "with no extractable seam," and called the refactor "a real refactor [belonging] to its own lane." An input arc that skips the seam ships blind. **Why the standard never caught this drifting.** `COHERENCE.md` §16 grades the *semantic protocol* — degradation practiced, capability negotiation, versioning — and that grade (Strong) is earned. But **no scorecard row measures the GUI as a product**, and **the golden journey has only ever been graded on the TUI**. "Semantic frontend: Strong" and "I'd use a different editor before the GUI" stayed simultaneously true because the standard only measured the first. This is §1.1's substrate-without-surface at frontend scale: the substrate is the protocol and the daemon's facts; the missing surface is the GPU consumers of them (§2.2 below shows which halves exist). Stage 0 closes the measurement gap so it cannot reopen. --- ## 2. Ground truth (audited 2026-08-09) ### 2.1 What the GPU has Code area with syntax/LSP styling, gutter (Off/Absolute/Relative/ Hybrid) with diagnostic signs, minimap with click/scrub, one bottom status band (statusline segments validated and themed), bottom panel band with divider drag, minibuffer with a 10-row candidate dropdown, in-buffer completion popup with kind glyphs, right-click context menu, isearch band UI, diagnostic squiggles (dedicated pipeline), selection and search washes, peer presence (cursors + selections), inline math, terminal mode, optimistic CRDT editing with unconfirmed-edit journaling. Mouse: click, drag, double/triple-click, wheel (line- quantized), edge auto-scroll, panel and divider gestures, minimap scrub. Clipboard both directions via `arboard`. ### 2.2 Wire-capability matrix: produced vs consumed The GPU's live-loop catch-all is one `_ => None` (`main.rs:5211`); `FoldState` and `BlockAdornments` appear in `pmacs-gpu` **only** in a debug-name helper. But "the GPU ignores it" means different work depending on whether a producer exists — revision 1 conflated these, and three items hid producer scope. Producer column verified against `src/semantic_render.rs` at `4bc55e8`: | Capability | Produced? | GPU consumes? | Work required | |---|---|---|---| | `FoldState` | **Yes** (`semantic_render.rs:1881`) | No | **Consumer-only** — Stage 3a | | `InlineAdornments` | Only `(AtOffset, Text)`, from the inlay-hint store (`semantic_render.rs:1849`) | Exactly that same subset | **No live gap today.** Other placements/content are producer *and* consumer work; not claimed by this arc | | `BlockAdornments` | **No** — a producer test asserts none is emitted (`semantic_render.rs:4543`) | No | Producer + consumer; Stage 3a's framing decides whether GPU folding renders from `FoldState` alone or needs placeholders | | `ResourceOffer` / `AdornmentContent::Resource` | **No** | No | Producer + consumer; stays deferred (§6) | | `InstanceSignal::Title` | **No producer found** in `src/` | No | **Not needed for a dynamic window title** — `StatusFacts` already carries the buffer name, so Stage 1c titles the window frontend-locally; a `Title` producer (e.g. terminal-set titles) is separate, unclaimed work | | `InstanceSignal::Bell` | **Yes** (`daemon.rs:1373`, present at `4bc55e8`) | No — `frontend.rs:349` is the grid consumer; `pmacs-gpu` has no arm | **Consumer-only** — Stage 1c. *Revision 2 recorded "no producer found" and gave Stage 1c a producer question to answer; the producer was there the whole time, and the audit had searched the semantic-render path rather than the daemon's signal path.* | | `Goodbye(reason)` post-handshake | Yes | Bootstrap only; live-loop reason discarded | **Consumer-only** — Stage 1c | | `CompletionPopup.prefix_len`/`total` | Yes (on the wire) | Stored under `#[allow(dead_code)]`, unrendered | **Consumer-only** — minibuffer/completion refinement | ### 2.3 Input gaps (verified in-session, not carried from docs) - **Escape quits the entire application** when no intercept/popup is active (`main.rs:2769`; the comment says "otherwise it stays the local quit"). No data is lost — the daemon holds state — but it reads as a crash to anyone with Escape reflexes. - `translate_key` produces **no `ProtocolKey::F(u8)`** — F1–F12 are unbindable in the GUI though the protocol carries them. `BackTab`, `Menu` also unmapped; `Key::Dead(_) → None` (dead keys silently dropped); multi-codepoint `Key::Character` truncated to its first char. - **No `WindowEvent::Ime`, no `set_ime_allowed`** — CJK/compose input is unusable. Undocumented anywhere before this audit. - **No `ScaleFactorChanged` arm; `scale: 1.0` hardcoded** — HiDPI is wrong (also recorded as a pre-existing gap in `gpu-set-font-framing.md`). - Sub-line wheel deltas are rounded then discarded with **no residual accumulator** — precise-pixel trackpad scroll under ~½ line height does nothing. Horizontal wheel x is discarded although `code_scroll_left` exists; `MouseKind::ScrollLeft/Right` are never emitted. Ctrl+wheel is ignored. - No middle-click paste, no `DroppedFile`, no I-beam cursor over text, minibuffer dropdown not clickable (audit F-007). - **`FrontendEvent::FocusGained`/`FocusLost`/`Detach` are never sent** — no `Focused` arm; `CloseRequested` exits without `Detach`, so the daemon learns of departure by socket EOF. ### 2.4 Structure, robustness, chrome - One document window forever (§1 cause 1). Daemon splits invisible. - No auto-reconnect; the reconnect banner is an attach-TUI-only seam (`Frontend::draw_status_overlay`). F-008 in `gpu-attach-robustness-framing.md`. - **Session restore is structurally never**: desktop save/restore early-returns in Rust under a daemon (Q#DS9), and the GPU is always semantic — so a GPU session can never restore. Journey step 12's thin end, at its thinnest on this frontend. - Chrome theming is half-applied: `MENU_BG`, completion popup background, `MINIMAP_BG`, `CARET_COLOR`, `WINDOW_BG_RGBA`, and the peer-presence palette are hardcoded constants; custom themes fracture in the GUI. (The TUI's completion popup and menu are also unthemed `Indexed` constants — the pair should be fixed together, per no-privileged-frontend.) - Cursor: fixed 2px bar, fixed color, no blink, no styles. - Word wrap regressed at #221: the GPU had cosmic-text `WordOrGlyph` since it existed and now gets `Wrap::Glyph`; the long-lines framing already names `ui.line-wrap = "word"` as the clean additive third value. - LSP styling diverges by model: the grid **merges** LSP tokens over tree-sitter (`LspStyleView`/`merge_styles`); the semantic wire is single-authority — GUI highlighting is strictly poorer in mixed-authority languages. - `HoverView` and `SignatureView` exist in the core, **built and never attached anywhere** (§1.1 dark matter) — relevant to Stage 5. ### 2.5 Already-recorded backlog: mapped or explicitly left standing Revision 2 said this arc "sequences" `docs/side-quest-backlog.md` §"GPU frontend mechanics (non-theme)" without restating it, which claimed coverage it did not have — several items had no stage *and* no deferral, and a reader checking whether the arc covered their complaint had nothing to check. **Every item in that section is below. An item is either mapped to a stage or explicitly left in the standing backlog; there is no third state.** Handoff §6's capability-aware keymap item and the folding framings' Stage 3 obligations are absorbed **by reference** — each keeps its own framing. | Backlog item | Disposition | |---|---| | Command/minibuffer chord forwarding; Meta/Super chords | Stage 1a | | Rebindable local `Ctrl-V`/`Escape` | Escape half → Stage 1a; `Ctrl-V` half → Stage 2 (it is a keymap-vocabulary question, not an input-plumbing one) | | Middle-click paste | Stage 1b | | Right-click context menu | **Already shipped** (§2.1) — the backlog item is stale and Stage 0 retires the line | | Frontend-local provisional selection | **Standing backlog** — a selection-ownership question, not a GUI-maturity gap | | Minibuffer `i/total` hint (= `CompletionPopup.prefix_len`/`total`) | Stage 3d | | Clickable minibuffer dropdown rows (audit F-007) | Stage 3d | | Multibyte-exact band caret; nav highlight-wrap bug | Stage 3d | | Telescope-style preview pane; candidate kind/doc annotations; unify TUI inline vs GPU dropdown | **Standing backlog** — the unification is a cross-frontend convergence design, and the other two ride it | | Scrollbar scroll; pixel-smooth sub-line scroll | Stage 7 (the *discard* bug is Stage 1b; the smooth **model** needs Stage 7's facts) | | Horizontal scroll / soft-wrap | wheel → Stage 1b; wrap → Stage 3b | | Auto-reconnect + "reconnecting…" banner | Stage 4a | | `AttachRequest.initial_size` cell-grid assumption | Stage 1c, with DPI — the assumption is only visible once scale is real | | Capability renegotiation (relaunch daemon `--features crdt`) | **Standing backlog** — daemon lifecycle, not frontend maturity | | Peer caret glyph + name label; own-vs-peer cursor merge; `SelectionSnapshot` vs `Decorations::Selection`; background-kind decorations painted | **Standing backlog** — collaboration/decoration rendering; no journey step and no §3.1 blocker depends on them | | Inline adornment placements beyond `AtOffset` | **Standing backlog** — producer *and* consumer scope (§2.2) | | Glyphon full-buffer `prepare` ceiling; `Renderer` sub-struct extraction | **Standing backlog** — perf and refactor; the `main.rs` split's first slice is Stage 1-pre and claims no more | | Golden-PNG comparison harness | **Deferred by §8**, with its condition stated there | Three gaps from §2.3/§2.4 are not in that backlog section and are mapped here so they cannot fall through: **`DroppedFile`** → Stage 1b; **cursor blink and styles** → Stage 3c (§7 registers the knob, and Stage 3c is what ships it — revision 2 named the configuration with no stage behind it); **the LSP merge-vs-single-authority divergence** (§2.4) → **standing backlog**, explicitly, because it is a semantic-wire authority question whose fix belongs to whoever owns multi-server token policy, not to a GUI maturity stage. --- ## 3. Closure criterion (Q#GA2) **The arc closes when all three of the following hold; none alone is sufficient:** 1. **Every stage in §5 has landed** — or has been explicitly re-ruled by the user at the time, with the ruling and its reason recorded in §3.2. There is no stage outside the closure contract: revision 1's "Tail" is dissolved, and the Q#GA3 goals are Stages **5, 7, 9 and 10** (hover/signature; smooth scroll + scrollbar; the project/files sidebar; tabs). *Revision 2's sentence said "Stages 5, 6 and 10" while §5 marked four stages as Q#GA3 goals — the sidebar was absent from the closure sentence that is supposed to enumerate them.* 2. **The per-frontend journey table shows GPU ≥ local TUI at every step**, under the grading rules in §3.3. 3. **The daily-driver blocker list (§3.1) is empty.** Divergences that survive must be declared in §3.2, not accidental. ### 3.1 Blocker list (seed — membership is Q#GA11) 1. Escape quits the application (§2.3). 2. IME absent — CJK/compose input unusable. 3. `translate_key` holes: F-keys, BackTab, dead keys, multi-codepoint text. 4. Sub-line scroll discard (trackpad feels broken); no horizontal wheel. 5. No DPI/scale handling. 6. Folding silently dead on the GPU. 7. No session restore on the GPU, ever (Q#DS9). 8. No reconnect after daemon restart. 9. One-window ceiling (graded via the journey table's affected steps rather than as a single line — listed here so the list cannot be emptied while the ceiling stands). ### 3.2 Declared-divergence and re-ruling register Divergences that survive the arc, and any stage the user re-rules out of the closure contract, are recorded here with a reason (the model is #221's honest-divergence ruling on word wrap). Seed: none — entries are added by stage framings or user rulings as they happen. ### 3.3 How a (step, frontend) cell is graded `COHERENCE.md` §2's existing verdicts are compound strings ("Works but undiscoverable", "Partial (good once reached)") and do not order. Revision 2 replaced them with an ordered set but never said how a cell *acquires* a grade, and its normalization rule — take the head grade, demote the rest to annotation — is unsound in the exact case it was written for: **"Works but undiscoverable" would grade `Works`**, and the undiscoverability would become prose that cannot block closure. That inverts the standard, because discoverability is not a qualifier on journey steps 4, 7 and 11 — it *is* their substance. **Three columns, not two.** Stage 0 grades each step for **local TUI** (`pmacs .`), **attached TUI** (`pmacs --attach`) and **GPU** separately. **The comparison is GPU against the local TUI.** That is the canonical `pmacs .` journey and the frontend the daily-driver report calls suitable, so it is the bar the GUI must meet. **The attached TUI column is retained as evidence, not as the bar — and what it is evidence *of* is narrower than revision 3 first claimed.** The attached TUI is a **grid** frontend: it handshakes `semantic_render: false`, and the field's own comment says it "never consumes the SemanticFrame family" (`src/attach.rs`, the `FrontendCapabilities` constructor). So a shared GPU/attached-TUI gap cannot mean "the semantic wire is at fault" — the attached TUI is not on that wire. What the three columns actually separate is: - **local vs daemon-attached** behaviour (local TUI against attached TUI), which isolates everything the daemon boundary introduces; and - **attached-grid vs semantic/GPU** behaviour (attached TUI against GPU), which isolates what is specific to semantic rendering. **Neither comparison alone establishes producer-versus-consumer ownership.** Reading the columns narrows where to look; **source tracing is what assigns the gap**, exactly as §2.2's matrix had to be verified against `src/semantic_render.rs` rather than inferred from behaviour. A single TUI column would still have merged two distinct diagnoses — that argument survives — but it was never going to hand out owners for free. **The grades, by observable criteria.** Each is a test someone else can run and get the same answer: > **Broken < Missing < Partial < Works** - **Works** — every required subclaim holds with no qualifier, by a route the step's own discoverability subclaim admits. - **Partial** — every required subclaim is *satisfiable*, but at least one is degraded: reachable only by a route the step does not admit (e.g. only by typing an unlisted command), or holding only under a stated precondition. - **Missing** — a required subclaim has **no surface at all**: the action is unavailable and attempting it produces neither effect nor error. - **Broken** — a surface exists and using it produces a **wrong result**, data loss, or an application-level failure. Ranked *below* `Missing` deliberately: an absent feature is honest, while a present one that misleads costs the user work and trust. **A compound step is graded by its weakest required subclaim.** Each step in the table declares its required subclaims explicitly; the cell's grade is the **minimum** over them, never the first word of a prose verdict. The worked examples are not hypothetical — they are the two rows `COHERENCE.md` §2 carries today: - **Step 7** — "Symbol: **works but undiscoverable**". Subclaims *reachable* / *discoverable*: `Works` and `Missing`. Cell grade **`Missing`**. - **Step 11** — "**Works but undiscoverable**". Same shape, same result. Under revision 2's head-grade rule **both would have graded `Works`**, and the undiscoverability that is the entire finding would have become annotation text with no effect on closure. Two of the journey's twelve steps is not an edge case. **Annotations carry only what is not a required subclaim.** They cannot absorb a failing subclaim; if something is load-bearing enough to mention as a defect, it is load-bearing enough to be a subclaim and be graded. Where the two frontends differ only in an annotation, the difference is recorded and does not block closure — that remains true, and is now narrow rather than a loophole. **Stage 0 must publish the subclaim list per step**, not just the grades. A grade whose subclaims are unstated is not falsifiable, which is the whole objection this section answers. --- ## 4. Arc structure (Q#GA1) **One arc, two halves; the name is "the GUI arc," deliberately a name and not a number.** The roadmap's "Arc 8 — GPU structural parity" label already collides (the Lean 4 framing also claims Arc 8; the collision is recorded in `docs/dired-framing.md`). This arc subsumes roadmap-Arc-8's scope as its Half B; the numeric label retires. - **Half A — maturity** (Stages 0–5): the GPU behaves like a competent desktop application over its existing one-window model. No structural redesign; heavy protocol work only where §2.2 shows a producer already exists, or the stage's framing names the producer scope it adds. - **Half B — structure** (Stages 6–10): the multi-window model, then the viewport/window-identity substrate it defines, then splits and side geometry, then the presentation stages that depend on multi-window state (sidebar, tabs). The order is load-bearing — see the note opening Half B in §5. Half A ships visible value while Half B's model framing matures; the arc does not close at the end of Half A (§3's condition 1 spans both halves), so the early wins cannot quietly become the whole arc. --- ## 5. Stage map **Stages 1–10 each get their own framing before their branch; Stage 0 is framed by this document** (see the status block). This document owns the ordering rationale and the arc-level bar, never stage-level acceptance criteria (the contract-ownership rule). **Every PR in this arc opens with its `docs/active-work.md` lane written at the branch's first commit** — the standing correction from #171/#215, missed again at #224 and #225, and adopted here as an arc rule rather than re-hoped. ### 5.1 The P2 gate (blocking №2's resolution) Two stages are **workspace-owned** and carry a hard gate: they may not start before the P2 workspace arc has landed at least the workspace object they consume. - **Stage 4b (session save *and* restore)**: "what a session *is*" is the workspace question — Q#DS9 failed precisely because a daemon layout had "nothing principled to attach to" (`COHERENCE.md` §7). A frontend-keyed convention invented here would be a new ownership story P2 then has to unwind; revision 1 called that v1 "plausible", revision 2 withdrew the recommendation, and Q#GA10 is now **ruled** (both surfaces preserved, the save path owned here too). The gate is what makes the ruling implementable: snapshot ownership and multi-frontend arbitration have no answer without P2's object. - **Stage 9 (project/files sidebar)**: a sidebar must show *something rooted*, and §7 warns P2 must start "before a fifth subsystem grows its own root convention — four have already diverged." The sidebar is the fifth if it picks its own root. **Reaching Stage 4b is a P2 START GATE, not merely a pause** (Q#GA5 ruling, revision 3). Revision 2 let the gated stage stall while everything else proceeded. **It could not have let the arc formally close around P2** — the gated stages are inside the closure contract (§3, condition 1), so closure still blocked on them. What it *would* have allowed is every **non-gated** stage finishing before P2 began, leaving P2 as a **terminal closure blocker**: an arc sitting at 100% of the work it could do, waiting on an arc nobody had started. The rule is stronger: 1. When the arc reaches Stage 4b, **P2 starts**. That is the trigger. 2. **No later GUI stage starts** — gated or not — until P2 has **an approved framing and an opened lane**. Those two are the observable condition; P2 need not have *landed* anything. 3. Once P2 has both, **non-gated GUI work may interleave** freely while the workspace object lands. Only the two gated stages (4b, 9) wait on the object itself. The gate is on *starting P2*, not on P2's completion, so the arc is never blocked on work nobody has begun — and it cannot outrun the model it depends on. A gated stage never proceeds on a local convention; that was already true and stays true. The arc's `docs/active-work.md` lane records the gate state whenever it is in force. ### Half A — maturity **Stage 0 — the standard sees the GUI (docs only).** *The framing goes first, and that reverses revision 2's ordering.* Revision 2 put the absorption PR ahead of committing this document, which contradicts the portability rule it cites elsewhere: an approved framing that lives only in one worktree is one `git clean` from gone and does not travel to another machine. **The approved framing and this arc's `docs/active-work.md` lane are the Stage 0 branch's first commit.** If synchronization stays a separate PR, **that PR carries the framing first** — absorption may precede the rest of Stage 0, never the framing. *The absorption pass*, whose scope is now enumerated rather than described (it grew on 2026-08-10 when six lanes merged in one session): - **Five stale lanes in `docs/active-work.md`** — #224 and #225 carried as OPEN, #228 as OPEN and MERGE-BLOCKED, LSP LaTeX as "no PR yet" (merged as #230), destination capture as "PR #231 OPEN" (merged as `0e4c58d`). Durable facts into `docs/agent-handoff.md` first, then remove the **PR-specific** block. **#228 is the exception, and it must not be retired "per Rule 4" as though Discovery were finished.** Rule 4 removes a lane when its **arc** is done; Discovery's is not. Two entries exist — the PR-specific block and the standing **Discovery lane (P4)**, which already says "Rewritten, not removed" for exactly this reason. Stage 0 removes the first after re-homing its facts and **rewrites and coalesces the second** to *"Stage 2 merged; later discovery work remains"*. Still open there: **predicate evaluation**, **command metadata** (title/category/aliases/flags), **help unification**, and **the prefix decision**. Deleting that lane would drop four named pieces of open work on the strength of one merged stage. - **The authority/recovery anchor**, which points at `9a26ac8` while `main` has moved well past the audit anchor `4bc55e8`. - **`COHERENCE.md` §0 row 16 / §16's `v6..=v21` → `v6..=v23`.** The ceiling moved **twice**: #221 took it to v22 for `LineWrapFacts`, and **#228 took it to v23** for `MinibufferPromptRows` (`PROTOCOL_VERSION = 23`, `SUPPORTED_PROTOCOL_VERSIONS = 6..=23`, `pmacs-protocol/src/message.rs:1843`). *Revisions 2 and 3 both said v22, having read the range at the audit anchor and not re-read it after Discovery landed.* The same row's "production attach remains v20" is **still correct** — `ADVERTISED_PROTOCOL_VERSION` is 20 — and must not be swept along with the range. - **The U4 correction and the U9 residue** in `docs/ci-red-signatures.md`. U4's flavour field is not a matching key (the same selector and fragments red on both macOS flavours) and one of its four "occurrences" was a deliberate bite. **U9's text must be fixed, not merely carried**: it says a same-tree green shows the failure "is not the tree," which contradicts this file's own rerun rule — a tree can raise an intermittent failure *rate* without making it deterministic. The replacement claim is **"not deterministic on this tree; causation and rate effect unresolved."** - **The stale backlog line** for the right-click context menu, which ships (§2.5). - **Journey step 11's verdict**, which #232 falsified on 2026-08-10. `COHERENCE.md` §2 still reads "**Works but undiscoverable** … no keybinding, no statusline spinner/progress indicator anywhere (§9)". #232 shipped exactly that indicator — a statusline provider showing an in-flight count and the oldest job's purpose, absent when idle. The row needs regrading under §3.3, and §9's own grade needs re-reading: the mechanism-without-identity finding is partly answered. **Found while grading the journey for this revision, not in review** — which is the argument for §3.3's three columns, since a stale row survives precisely as long as nobody has to assign it a falsifiable grade. *Then Stage 0 proper:* add the per-frontend journey verdicts to `COHERENCE.md` §2 under §3.3's grading rules, **including the subclaim list per step**; place the arc in §20 (Q#GA5); cross-reference from `docs/agent-handoff.md` §6; retire the "Arc 8" numbering (Q#GA4). No runtime code. **No orphan scorecard row.** Revision 2 proposed adding a GUI-product row to the scorecard with a grade attached ("Weak — renderer ahead, workbench and input behind"). A scorecard row is a pointer to a graded concern, and there is no graded concern for the GUI *as a product*: `COHERENCE.md` §16's grade is architectural. So Stage 0 first establishes **a distinct product subgrade beside the architectural one in §16**, with its own criteria and audited ground truth, and *then* the scorecard points at it. A row whose grade rests on nothing is the thing §16 exists to prevent. **Stage 1 — input foundation.** - **1-pre: the input seam.** Extract `App::window_event`'s routing into testable functions — the refactor `gpu-terminal-input` already named as its own lane, plus the first slice of the recorded `main.rs` split. This is the stage's first PR because everything after it needs witnesses. - **1a keyboard correctness**: Escape stops being the local quit (round-trips like any key; quitting becomes a command/window affordance — subsumes the backlog's "rebindable local Ctrl-V/Escape" item on the Escape half); `translate_key` completion (`F(u8)`, `BackTab`, `Menu`; dead keys held for 1d rather than dropped). - **1b pointer/scroll correctness**: sub-line residual accumulator; horizontal wheel → `code_scroll_left`; middle-click paste; I-beam cursor over text; `DroppedFile`. **Q#GA6 — RULED: land the TUI answer in this same stage; no declared divergence.** The TUI half is smaller than revision 2 implied, and the corrected trace is this: crossterm already delivers `MouseEventKind::ScrollLeft`/`ScrollRight`; `src/protocol.rs` and `pmacs-protocol` already carry them as `MouseKind::ScrollLeft`/ `ScrollRight`; and **attached** mouse events already round-trip through `mouse_from_crossterm` / `mouse_to_crossterm` (`src/protocol.rs:712`, `:777`). Local and attached document events **converge on the one document handler, whose only wheel arms are `ScrollUp`/`ScrollDown` at `src/editor.rs:3189`** — that single site is where horizontal wheel is dropped. *Revision 3 cited `src/editor.rs:5863-5864` and `:3407` here. Both are **terminal-content** paths — `:3407` matches `TerminalMouseKind` and drives `terminal_manager.scroll_view` — not the document window, so they were the wrong sites for this ruling.* So the TUI answer is one handler arm on an event that already arrives, not new plumbing, and QoL Stage 5's reason for excluding explicit-scroll surfaces — keeping the frontends agreeing — is *served* by doing both here rather than traded against. - **1c session/window signals**: `Focused` → `FocusGained`/`FocusLost`; `CloseRequested` sends `Detach` before exit; post-handshake `Goodbye` reason surfaced (consumer-only, §2.2); **window title composed frontend-locally from `StatusFacts`** — no `Title` producer required (§2.2); **`Bell` as a plain consumer** — the producer exists and always did (§2.2), so there is no producer question and no option to drop it; `ScaleFactorChanged`/DPI, and with it the `AttachRequest.initial_size` cell-grid assumption, which is only observable once scale is real. - **1d IME — Q#GA7 RULED: the full preedit overlay, not a commit-string minimum.** The scope is explicitly: commit string; **caret and selection range within the preedit**; **cancellation**; and **focus-loss cleanup** so a dropped composition cannot survive as stale overlay text. Preedit needs a rendering surface, which is why this is not folded into 1a — and why the ruling has real cost, stated rather than discovered later. **Stage 2 — capability-aware keymap resolution.** **By reference, not absorption**: handoff §6 requires this to be its own framing round and forbids starting it as a half-lane. This arc sequences it here because Stage 1's seam makes its GPU consumers testable, and consumes it for default zoom bindings and every future GPU-native chord. **This arc feeds Stage 2's framing one explicit input question: does the capability-aware vocabulary cover pointer gestures (wheel-with-modifier), or keys only?** Revision 1 assumed the former; nothing yet guarantees it (major №4). **Q#GA8 — RULED: wait for Stage 2. The temporary Ctrl+wheel zoom island is not created.** Zoom arrives through this stage's mechanism or not at all. Consequently this arc adds **no** off-path hardcode, and §7's island accounting is now unconditional rather than "at most one" — there is no removal criterion to track because there is nothing to remove. **Stage 3 — parity consumers.** - **3a folding Stage 3** — consumer-only per §2.2's matrix (the producer exists). The obligations are already enumerated in the folding framings (a `FoldState` consumer, the fold-mirror clear on `BufferSnapshot` (R2-4), `fold_projection` flip, optimistic-edit unfold (R2-3), caret/hit-test fold-awareness). Whether GPU folding needs `BlockAdornments` placeholders — which would add producer scope — is that framing's question. Its ordering precondition (bottom-panel Stage 2's landed band) is satisfied. - **3b word wrap** — `ui.line-wrap = "word"` as the declared third value. Nearly free on the GPU (cosmic-text `WordOrGlyph`). **Q#GA9 — RULED: implement the grid answer with UAX #14; no declared divergence.** The dependency is accepted rather than traded for a §3.2 entry, so both frontends wrap by the same rules. - **3c chrome theming** — `ThemeFacts` adoption for menu, completion popup, minimap, caret, window background, peer palette; the TUI's unthemed popup/menu pair is fixed in the same stage or declared. **Cursor blink and cursor styles ship here** — §7 registers the knob through the config registry and this is the stage behind it (§2.5). - **3d minibuffer/completion refinements** — `prefix_len`/`total` rendered (consumer-only, already on the wire per §2.2); clickable dropdown rows (audit F-007); multibyte-exact band caret; the nav highlight-wrap bug. Grouped as its own sub-stage rather than scattered, because §2.5 showed four backlog items landing in one surface. **Stage 4 — robustness.** - **4a auto-reconnect** + a reconnecting banner (parity with the attach TUI's seam; F-008). This adds a background reconnect loop and therefore owes §20's background-work attribution — see §7 for the contract its framing must satisfy (owner, lifetime, cancellation, failure attribution). - **4b session save *and* restore for semantic frontends** (Q#DS9) — **P2-gated, §5.1.** The stage owns **both halves as a pair**, which revision 2 got wrong by naming only restore. Q#DS9 scopes v1 to the local `editor::run` path and makes `desktop_mode(true)` auto-save **and** auto-restore no-ops in daemon mode, enforced in Rust by a `DaemonMode` marker that `save_session`/`restore_session` early-return on. So there is no snapshot being written under a daemon today: a restore path alone would have nothing to read, and shipping restore without save would be a stage that cannot work by construction. **Q#GA10 — RULED: preserve both surfaces.** Automatic restore on the **first eligible attach** when armed and no explicit target was supplied, **plus** the existing explicit command — not one or the other. What remains open is more than revision 2 admitted when it said only the trigger shape was: **snapshot ownership** (who writes it, keyed how, once the frontend is not the owner), **save timing** (before-quit is a local-mode assumption; a daemon frontend can detach without quitting anything), and **multi-frontend arbitration** (two attached frontends with divergent layouts and one workspace key). All three are decided in this stage's framing, on P2's object. **Stage 5 — hover/signature popups** (Q#GA3 goal). The core's never-attached `HoverView`/`SignatureView` are the data-model precedent; the GPU needs a popup surface and a wire decision (ride an existing family vs a new message — its framing decides; hover data currently flows Lua → echo/`*lsp-help*`, so there is **producer scope here by construction**, stated rather than hidden). Independent of Half B; sequenced after Stage 3 so the popup is themed from birth. ### Half B — structure **Revision 3 reorders this half.** Revision 2 ran viewport facts (6) before the multi-window model (7), which is backwards twice over. A viewport fact is *about* something — a window — and whether a semantic window is a **daemon projection** or a **frontend-local object** is exactly what the model stage decides; designing the facts first would fix an identity the model then has to honour or break. Second, revision 2's model stage was **framing-only** yet the sidebar was told to "ride Stage 7's geometry": a framing produces no geometry, so Stage 9 consumed something no stage shipped. Non-bottom side geometry is now **implemented** in Stage 8. **Stage 6 — the multi-window model framing.** The arc's center of gravity and the reason Half B exists: how a daemon layout projects to a semantic frontend (project the per-frontend layout tree vs frontend-local layout over multiple buffer subscriptions — the wire today assumes one document window per semantic session, with the panel band as the only exception). **Its output that everything downstream needs is the window-identity decision**, because that is what a viewport fact, a split, a side slot and a tab all refer to. Framing only; it ships no runtime code, and nothing downstream is told to consume geometry from it. **Stage 7 — the viewport/window-identity substrate, and the scroll feel that reads it** (Q#GA3 goal). Viewport facts on the wire, carrying the identity Stage 6 settled — **the GPU never consumes daemon `view_top` today**, which the backlog names as the blocker for recenter and every scroll command (`docs/side-quest-backlog.md:147`; revision 2 stated this exactly backwards). **Smooth scroll and the scrollbar live here**, not in a tail: a scrollbar needs authoritative extent and position, and pixel-smooth scrolling changes the scroll model those facts feed. **Stage 8 — splits shipped, and side geometry with them**: rendering, input routing, per-window status bands, window-command parity (`C-x 2/3/o/0/1`), **plus implemented side-window geometry beyond `Side::Bottom`**. The geometry is here rather than in Stage 6 because it is code, and because Stage 9 consumes it. **Stage 9 — the project/files sidebar** (Q#GA3 goal) — **P2-gated, §5.1.** Tree-primitive adoption (`COHERENCE.md` §14 names project files as a future tree consumer; §3 names the surface). Rides **Stage 8's implemented geometry** and P2's root object. **Stage 10 — tabs/tabline** (Q#GA3 goal — last, low priority by ruling). **Q#GA12 — RULED: a deliberate deferral to this stage**, decided after P2 and the multi-window model exist, because both are what make the readings meaningful — the lineage precedents disagree (Emacs `tab-bar-mode` tabs are **window configurations**; tab lines and Doom's centaur-tabs are **buffers**), and a workspace-keyed third reading only becomes available once P2 has landed. Deferring is the ruling, not an absence of one. The constraints hold regardless and are pinned now: tabs present **existing objects** (whichever kind), **never a parallel registry** with unvalidated references — the menu-label mistake is the named anti-pattern — and the surface is **optional and off by default**. --- ## 6. Non-goals and named deferrals - **GUI as the default frontend.** Deliberately **not** the closure bar (Q#GA2 chose journey parity). It remains `gpu-initial-target-framing.md`'s deferral, to be *decided* — not assumed — when the arc closes. - **Git integration** (`COHERENCE.md` §15): editor-wide, not GUI-specific; not this arc. - **Settings/preferences GUI**, **native menu bar**, **multiple OS windows**: out of scope; nothing below depends on them. - **`ResourceOffer`/image rendering**: unproduced and unconsumed (§2.2); stays deferred unless a stage (sidebar icons, hover docs) pulls it in with a framing that owns both halves. - **Proportional code fonts, ligature/feature toggles, font wire transfer**: `gpu-set-font-framing.md`'s deferrals stand. - **Remote GPU paths, daemon service management**: unchanged. - **Multi-cursor**: pre-existing v0.1 non-goal, unchanged. --- ## 7. Coherence impact (per `CLAUDE.md` / `COHERENCE.md` §20) - **Journey steps touched**: **3, 4, 5, 6, 7, 8, 10, 12** — *on the GPU frontend*; Stage 0 makes the journey frontend-graded so the impact is measured per step rather than asserted. Revision 3 listed five; three were missing because the list was carried from revision 1's smaller stage map and never re-derived against the stages this arc actually ships. **Step 3** — `DroppedFile` (Stage 1b) is an open-a-file route. **Step 5** — keyboard correctness, IME, folding, wrapping and scrolling are all editing-surface work (Stages 1a, 1d, 3a, 3b, 7). **Step 6** — completion refinements and hover/signature popups (Stages 3d, 5). - **Interaction islands**: this arc adds **no dispatch shadows** — the count stays at six — and, after Q#GA8's ruling, **no off-path hardcode either**. Revision 2 reserved one temporary island for the Ctrl+wheel zoom intercept under a mandatory removal criterion; the ruling declined it, so the census is untouched by this arc and there is no removal criterion to track. Zoom arrives through Stage 2's mechanism or not at all. A stage that believes it needs a new *shadow*, or a new island, returns to this document first. - **Config registry adoption**: every user-visible knob this arc adds (smooth scroll, scrollbar, cursor blink, tabline toggle, IME behavior if any) registers through the config registry — no new raw Lua-table settings. The minimap's divergent tab width (4 vs the shared 8) stays owned by config-registry Q#CR13, referenced not absorbed. - **Background-work attribution** (moderate №8): Stage 4a's reconnect loop is background work and owes the §20 attribution regardless of §9's unsolved general model. The contract its framing must satisfy: **owner** — the GPU frontend process, scoped to its session, never the daemon; **lifetime/cancellation** — bounded backoff, canceled on user quit and on successful re-attach, never outliving the window; **failure attribution** — every terminal failure surfaces in-window with a reason, and the contract covers the case where the daemon supplies none. **The reason requirement is two-sided, because the silent cases are the common ones.** Revision 2 required "the daemon's stated reason", which is unsatisfiable exactly when it matters: a daemon that **crashes or drops the socket delivers no `Goodbye` at all**, and the frontend learns of departure by EOF (§2.3 records that the GPU already loses its peer this way today). So: **use the daemon's reason when one arrives** — which is why Stage 1c's post-handshake `Goodbye`-reason consumer precedes this stage — **and otherwise surface an explicitly locally-classified transport/EOF reason**, labelled as locally inferred rather than reported. A banner that says nothing because the daemon said nothing is the failure this clause exists to prevent. §9's activity-indicator gap is *not* claimed by this arc. --- ## 8. Verification shape - **What already exists is used, not rebuilt** (major №5): the real offscreen `render_to_view` composition harness, the readback path, the smoke tests, and the required-GPU CI job are **landed**. Stages 3c, 8 and any pixel-visible change add pixel assertions against that harness immediately. What is deferred from `gpu-golden-harness-framing.md` is only **golden-PNG comparison and the case gallery**; a stage adopts those if image diffing beats direct assertions for its witnesses, with that framing. - **The a37 problem is confronted, not inherited.** Real-GPU end-to-end tests compile only when `pmacs-gpu` is built, return `ok` without running otherwise, and are load-sensitive — the recorded footing hazard. Stage 1-pre's seam exists so input stages are witnessed *without* a display; stages that genuinely need a real frontend say so and name their witness (`PMACS_REQUIRE_GPU` discipline). - **The arc ratchet**: extend `tests/journey_acceptance.rs` with GPU-frontend rows where headlessly drivable; stages add rows, none removes them — same rule as the existing ratchet. --- ## 9. Rulings — Q#GA4 through Q#GA12, all closed **Every arc-level question is ruled as of revision 3.** They are kept here with their answers rather than deleted, because a stage framing that wants to revisit one needs to see what was decided and why it is not open. - **Q#GA4 — RULED.** The name is "the GUI arc"; the numeric **Arc 8 label retires** at Stage 0, resolving the collision with the Lean 4 framing's claim on the same number. - **Q#GA5 — RULED, with a hardening.** Half A slots after P1. Reaching Stage 4b is a **P2 start gate**: no later GUI stage starts until P2 has an approved framing and an opened lane, after which non-gated work interleaves freely while the object lands (§5.1). Stronger than the recommendation carried in revision 2, which would have let every **non-gated** stage finish before P2 began — leaving P2 a terminal closure blocker rather than letting the arc close around it. - **Q#GA6 — RULED.** Land the TUI answer in Stage 1b; no declared divergence. The events already arrive and are dropped by the document-window handler (Stage 1b records the sites). - **Q#GA7 — RULED.** Full preedit overlay: commit string, caret and selection range, cancellation, focus-loss cleanup. Not the commit-string minimum. - **Q#GA8 — RULED.** Wait for Stage 2. **No temporary Ctrl+wheel island**, so this arc adds no off-path hardcode (§7). - **Q#GA9 — RULED.** Implement the grid answer with UAX #14. No declared divergence. - **Q#GA10 — RULED.** Preserve **both** surfaces: automatic restore on the first eligible attach when armed and no explicit target was supplied, plus the existing explicit command. Stage 4b owns the paired **save** path too, and three questions remain live inside that stage — snapshot ownership, save timing, multi-frontend arbitration. - **Q#GA11 — RULED.** The §3.1 blocker seed stands **unchanged at nine items**. - **Q#GA12 — RULED as a deliberate deferral** to Stage 10, taken after P2 and the multi-window model exist. The existing-object, no-parallel-registry and optional/off-by-default constraints hold from now, not from Stage 10. --- ## 10. Sequencing against #227 (git Stage 1) Settled with the user on 2026-08-10, and recorded here because it constrains when Stage 0 may start: 1. **Revision 3 → approval.** 2. **The approved framing and the Stage 0 lane are committed and pushed** on the Stage 0 branch, as its first commit. Until that happens this document is not portable and nothing downstream is safe to rely on. 3. **#227 is finished and merged** before Stage 0 implementation. 4. **Stage 0 rebases and performs the absorption**, which by then includes **#227's own newly merged lane** alongside the five already enumerated. The reason #227 goes first rather than riding alongside: its ref is **72 `main` commits behind**, and it touches `COHERENCE.md`, `docs/active-work.md` and `builtin/runtime/listview.lua` — the three files Stage 0's absorption rewrites. Carrying it across the arc would compound exactly the conflicts Stage 0 exists to retire.