PR #155 review round 2, self-review of the round-2 commit.
The round-2 change labelled "minor" — resolving both arms of
pmacs.window.buffer() through the acting frontend for uniformity — made
the NO-ARGUMENT arm fallible. `acting_frontend` follows the interactive
origin, which can name a frontend that has no registered view: a bare
`dispatch_key` from an unattached peer does exactly that. `selected_window`
then raises "acting frontend has no layout" instead of answering.
Nothing surfaced that error, because the runtime callers do not pcall it.
killring, syntax, autosave, pair, indent and comment all read
pmacs.window.buffer() on ordinary edits, so the raise silently dropped
the operation: kill_ring_acceptance went 30/30 to 25/5, with
frontend_detached_drops_per_frontend_state reporting only "B has kill
state". main is 30/30, and reverting this one file restored it.
The no-arg arm is back on ambient active_buffer_id() and now documents
why that is deliberate rather than an oversight: dispatch sets
active_frontend to the acting frontend before running a command, so the
two agree on every real path, while only the ambient resolver has the
fallback that makes it total. The explicit-window arm keeps its Q#BP11
layout validation, which is what the arc actually needed.
acc19c pins it through the real path — a buffer.after-edit subscriber
reading pmacs.window.buffer() during a viewless peer's dispatch_key —
rather than by calling the binding directly. Bite-verified:
scripts/bite bbe4152 src/lua_bindings/mod.rs --test
bottom_panel_stage1_acceptance -- acc19c goes red with the exact
"acting frontend has no layout" traceback.
The ledger also records two gating facts found on the way: the workspace
sweep must run with an isolated XDG_CONFIG_HOME, because the real user
init.lua installs a local package and the losing race leaks a status
message into painted-frame comparisons; and a latent pre-existing main
bug in the buffer CRDT undo path, which is not this branch's and whose
proptest seed is deliberately not committed here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012j4omtTMn9v1UfmHQb9ap6
The round-2 peer press landed in the peer's CONTENT area, so it never
reached `arm_window_drag` — the exact path Finding 5 names — and the
case bit nothing. It now presses the peer's own mode-line row, where a
single global drag slot is overwritten (and, since that lone window owns
no boundary, cleared outright).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR #155 review round 2.
Finding 1 (must fix): Q#BP7 item 1 — "growth reaching the live tail
re-arms follow (top -> None), only when no selection is active" — was
never implemented. `at_bottom` is the instantaneous geometric readout
`scroll_offset == 0`, which a still-anchored view satisfies whenever it
happens to be tall enough to reach the tail, so the round-1 assertion
could not see the gap: the next rows the child printed pushed the
anchored view back into history.
`rearm_follow_on_growth` now clears `top` when a viewport-size
declaration makes the view cover the tail and no selection is frozen,
and every size-declaring path (`snapshot_for_view`, `record_view_size`,
`view_status_for_size`) routes through one `declare_view_size` helper so
grid and semantic declarations cannot disagree. `scroll_view` and
`begin_selection` deliberately stay out: they write `top` themselves,
and `scroll_view` already owns the scroll-driven arm.
New acc32b is the pin the review asked for: scroll into history, grow
past the tail, then release a SECOND burst of child output through a
filesystem gate and assert the view moved with it.
Finding 2: the PTY fixtures emitted LF-only output, which staircases
rightward until every row clips to blanks past the viewport width — so
the round-1 anchor assertions compared "" with "" and could not fail.
Both fixtures now emit CRLF, and each anchor comparison is guarded by
`assert!(!top_before.is_empty())`.
Finding 3: acc33's contrast case asserted nothing, and the behavior it
claimed was false as coded. With the re-arm in place it is true and now
asserted: clearing the selection at the same geometry re-arms follow and
leaves the frozen anchor.
Finding 4: `start_run` gated the panel branch on `display == "panel" or
already_in_panel(..)`, so an explicit `display = "current"` lost to the
inference — and that value is the documented user-facing opt-out from
the Stage 3 default flip. Now gated on OMISSION. acc19b gains the
explicit-"current" case.
Finding 5: `window_drag` is a `HashMap<FrontendId, WindowDragState>`, so
a peer's mode-line press can no longer steal or clear another
frontend's in-flight gesture, and concurrent drags are legal. Cleared on
detach. acc30c gains the mode-line-press case.
Minor: `pmacs.window.buffer()` resolves both arms through the acting
frontend using the shared `lookup_window` / `selected_window` validators
rather than re-implementing them beside an ambient `active_buffer_id()`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`TerminalViewStatus.scroll_offset` is the retained rows between the
VIEWPORT and the live tail, so it necessarily tracks viewport height: an
assertion that it survives a panel height change unchanged is either
vacuous or wrong, and it went red once under a loaded sweep for exactly
that reason. Q#BP7's invariant is that the ANCHOR is frozen, so acc32
and acc33 now compare the first visible row's text across the change,
and additionally pin the follow behavior that distinguishes them: a
shrink never re-arms follow, growth reaching the tail does, and growth
with a frozen selection does not.
Both also wait for the child's last line before sampling, so neither
races further output.
Also records the round in docs/active-work.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR #155 review round 1.
Finding 1 (must fix): `try_split_active` had no production caller —
`pmacs.window.split_horizontal` / `split_vertical`, and therefore
`C-x 2` / `C-x 3`, still went through plain `split_active`. Splitting a
focused panel made the root wrapper's final child a split rather than
`Leaf(side)`, which both `Layout::compute`'s fixed pass and
`document_subtree` key on: the panel band reverted to 1:1 weight
division and an ordinary window ended up living inside it. Both bindings
now route through the guard, and acc26 asserts through the real Lua
path — a direct core call passes with the guard unwired, which is how it
survived the first round.
Finding 2: the armed-drag early return now checks the arming frontend,
so one frontend's in-flight gesture cannot cancel or swallow another's
mouse events. New acc30c.
Finding 3: `paint_mode_line_graphemes`'s doc block was left heading
`paint_divider_segment`; moved back.
Finding 4: a recompile carries no `display`, so it took the raw switch
and duplicated a panel-placed `*compilation*` into the document window.
`start_run` now detects that the buffer already owns the panel slot.
`pmacs.window.buffer` gained an optional window argument so an adopter
can ask without selecting the panel first. New acc19b.
Stage-2 hazard pins the review asked for, both in `src/daemon.rs`:
a fresh attach while LOCAL is focused in a panel inherits LOCAL's
document buffer, and an initial-target bootstrap whose `after-load`
hook creates and selects a panel still reasserts into a document window.
Minor: dropped listview's dead `p.side`; documented `focus_window`'s
caller-validates contract; `jump_back` restores through `focus_window`
so the "every focus change" contract holds; `params` / `resize` default
to the acting frontend's selected window rather than the ambient one;
widened the flexible-division math to u64 intermediates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the lane to docs/active-work.md: branch, base, what Stage 1
implemented, the verification run, and the two known local-only test
caveats (the parallel-load GPU flake and compile_mode_acceptance's
single-thread requirement).
The durable handoff snapshot stays untouched until the PR merges, per
its own update protocol.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- `listview.open`, `compile.run`, and `pmacs.terminal.open` all take the
same strict `display = "current" | "panel"`, validated before any
buffer, session, process, or wrapper exists. Omission keeps today's
behavior; Stage 3 flips the default.
- `listview.quit` / `compile.quit` delegate to `window.quit` only when
the buffer really is in a side window, so the presentation is deleted
or restored instead of leaving a source buffer stranded in the slot.
- LSP `visit_location`, LSP go-to-definition, and compile `visit_error`
route through `display_file`, so a visit from a panel lands in the
document target and fires its hook with that window active.
- `window.quit`'s Delete arm focuses the revalidated remembered origin.
- Capability fallback discards an accompanying `height` rather than
rejecting the call.
- `window.min-height` clamps a below-floor value on read instead of
refusing the write.
- `tests/bottom_panel_stage1_acceptance.rs`: 42 tests over the framing's
Stage 1 criteria, including the two production `Layout::compute`
callers, the recursive minima, hide/reappear, the final-focus matrix,
quit chains at the depth cap, per-frontend jump origins, the divider,
and a real-PTY pin of Bet B1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stage 1 substrate for the bottom-panel arc (docs/bottom-panel-framing.md).
- `WindowParams` (side / fixed_rows / dedicated + implementation-owned
quit action and remembered document origin), `Side`, `QuitAction` with
a bounded replacement history, and the `MIN_WINDOW_OUTER_ROWS` floor.
- `Layout::compute(area, fixed)` allocates fixed rows before dividing the
remainder by weight; both production callers feed the same shared map,
including the peer-presence overlay pass that derives its own rect.
- `subtree_min_rows` / `interactive_min_rows`: the recursive minima, and
`boundary_below` for the shared drag / keyboard resize boundary rule.
- `FrontendView` gains `panel_capable`, `frame_geometry`, and the derived
`panel_hidden`, each spelled explicitly at every construction site.
- `EditorCore`: `primary_document_window`, the non-side target rule,
`display_buffer` + placement policy, `quit_window`, side-window removal
on `kill_buffer`, per-frontend jump entries with origin windows, and the
shared resolve/load-without-switch seam the initial-target bootstrap now
uses too.
- `EditorState`: the panel reconciliation transaction, geometry
declaration, the side-window `dispatch_idle_for` gate, divider paint,
and divider drag.
- `pmacs.window.display / display_file / quit / panel / params /
set_params / resize / display_target`, plus `builtin/runtime/window.lua`
with `window.panel-height`, `window.min-height`, and the resize commands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the approved framing for the bottom-panel arc: a buffer displayed
in a fixed-height window pinned to the bottom of the frame, targeted by
policy rather than by stealing the selected window, plus the missing
display-buffer/window-parameter concept underneath it.
Revision 4 follows three review rounds, an integration review, and a
landed-state audit against GPU initial target (#148, protocol v20) and
folding Stage 2 (#149).
Amended before branching by the pre-implementation dependency
verification recorded in section 0.6:
- the folding dependency is cleared and re-verified against canonical
main at ddaa80d (nothing in flight, folding Stage 2 acceptance 48/48
green, every borrowed anchor reproducing, and folding's only window.rs
edit confined to one 22-line hunk that leaves the layout functions
pre-folding code);
- R5-B1: Layout::compute has TWO production callers, not one. The
second, the peer-presence overlay pass in src/overlay_paint.rs, builds
its own text-area rect from active_layout() and never routes through
window_placements, so the planned compute(area, fixed) signature change
would otherwise leave every peer cursor painted at its no-panel row.
Corrected in section 1.1 and Q#BP2, pinned by acceptance 1, and the
fixed map is now specified to come from one shared helper rather than
being assembled per call site.
Stage 1 (window placement + TUI side windows) changes no wire shape.
Move the lane from active-work.md into its Closed section, retire the
protocol v20 / main-hash references to LANDED form in agent-handoff.md, and
record both review-round lessons (failure-socket containment, upgrade-gated
replica publication) in the ops-lessons ledger.
18 of the last 60 CI runs failed (30%), including repeatedly on `main`.
Every failure is macOS-only and hits BOTH Lua flavors, so it is the
runner, not LuaJIT. Sampling 7 showed only two tests.
**m8_9 outline budget** — `outline_5_level_100_entry_renders_within_100ms`
is a wall-clock budget observed at 147ms and 149ms against 100ms on
GitHub's shared macOS runners, while Linux lands comfortably under. The
measurement and its printout now always run; only the ASSERTION is gated
on `!cfg!(target_os = "macos")`, exactly as
`composition_overhead_under_ten_percent` already is in `src/editor.rs`
for the same reason. A real regression still surfaces on Linux, on the
perf gates, and in the number printed to the log.
**vterm PTY smoke** — deliberately NOT a timeout bump. Instrumenting
locally showed the failing wait completes in 40ms against a 10s budget
(250x headroom), while the genuinely tight wait in the same test (2.86s
against 5s) never fails. Four hypotheses were eliminated with evidence:
- python3 cold start: the sibling test at :494 spawns the same
/usr/bin/python3 with a TIGHTER 5s budget and passes in the very
runs where the smoke fails ("3 passed; 1 failed");
- config resolution: XDG_CONFIG_HOME is read straight from the env
(src/config.rs:49), no platform branch;
- a stalled idle loop: the run loop polls on a 60Hz frame timeout and
ticks the supervisor every frame (src/editor.rs:2522), so child
output drains without input;
- a too-small budget: see the 250x headroom above.
The real defect this commit fixes is that NONE of those could be
distinguished from the CI log, which carried only "host output never
contained VTERM_ALT_READY" plus a tail of escape bytes. init.lua now
writes breadcrumbs (reached / terminal.open ok-or-error) and the timeout
reports whether pmacs is still alive plus each breadcrumb, so the next
occurrence names its own cause:
startup: pmacs still running; init.lua reached="1"; terminal.open="ok"
Verified non-vacuous: forcing the needle to never appear produces the
line above, which also proves io.open works in pmacs's Lua — otherwise
the breadcrumbs would silently read MISSING and mislead.
Test-only; no runtime code.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Integrate folding Stage 2 and its landed-state documentation with the
protocol-v20 GPU initial-target branch. Preserve per-session fold projection
selection in the target bootstrap transaction and retain v19 compatibility
coverage after the later protocol bump.
Shut down bootstrap sockets on every dispatcher-side failure and reject
frontend events whose session state was never installed. This prevents a
lingering failed client from reaching absent render/size state.
Track target-side CRDT upgrades independently from load/create status so a
deduplicated hidden buffer is published to every existing grid replica. Add
real-daemon regressions for both failure containment and replica publication.
Post-merge housekeeping owed from #149, kept as its own docs-only PR per
the #138-#140 / #147 convention. No runtime code.
- active-work.md: base snapshot and the recovery check bump 47581f4 ->
6ed4fe9. The Stage 2 lane is retired and replaced by a folding lane that
records both stages as merged with nothing in flight, and states Stage 3
(GPU) has no branch and no framing yet — carrying its named obligations
(GPU collapse at TUI parity, caret/hit-test fold-awareness, the
BufferSnapshot fold-mirror clear, CRDT-origin unfold, and flipping
FrontendView.fold_projection true for semantic frontends) as that
framing's starting point. "Closed since the last snapshot" gains #149
and #147.
- agent-handoff.md §1: main @ 6ed4fe9, the "Last updated" line and section
date, the Stage 2 bullet flipped from IMPLEMENTED/PR-OPEN to LANDED with
the Stage 3 obligations attached, and the roadmap entry (remaining arcs
now read "6 folding Stage 3").
Both stages' design points are recorded as traps Stage 3 inherits rather
than as history: the merged-hidden-component unit, per-window/per-target
map instances, per-frontend projection, position-not-row normalization,
and the post-intercept edit site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
PR #149 review round 5 flagged this PR as stale: it still claimed `main`
@ `c49a8c7`, Stage 2 framing "rev 2, under review; no implementation, no
PR", while `main` is `47581f4` and Stage 2 is implemented and open.
- Base snapshot and the recovery check bump `c49a8c7` -> `47581f4`.
- The folding Stage 2 lane becomes IMPLEMENTED / PR #149 OPEN: framing
rev 4 approved, the `VisibleLineMap` spine, the base-moved merge (and
why it was merged rather than rebased), and the five review rounds'
design-changing findings — each of which is a trap Stage 3 inherits.
- `main`'s ledger had gone unrefreshed through four merges, not one, so
"Closed since the last snapshot" now also records web grammars HTML +
CSS (#146) and LaTeX Stage 1 (#144) with its inline-math framing
(#145), including their durable lessons.
- agent-handoff §1: `main` @ `47581f4`, the "Last updated" line, the
Stage 2 substrate bullet, and the roadmap entry.
Rebased onto `47581f4` so it stays one documentation-only commit
directly off canonical main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Post-merge housekeeping owed from #142, kept as its own docs PR (no
runtime code).
- agent-handoff.md §1: bump main to c49a8c7, add the folding Stage 1
substrate bullet (store/View, structural source, C-c @ surface,
command-path unfold, FoldState production; no protocol bump), refresh
the "Last updated" line and the roadmap Arc 6 entry, and note Stage 2
is in framing on folding-tui (the visible-line-map reframe).
- active-work.md: retire the Stage 1 folding lane (PR #142 was OPEN),
add a "Closed since the last snapshot" entry for #142, open the
Stage 2 (grid/daemon collapse) framing lane on folding-tui, and
refresh the canonical base snapshot to c49a8c7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
PR #149 review round 5, finding 1 — correct, and both round-4 bugs did
survive through the intercept path.
Round 4 moved the widening off the point and onto `edit_start_of(&op)`,
but ran it in `run_buffer_edit` BEFORE `run_managed_edit`. A managed
buffer intercept may legally rewrite `pos` / `start` / `end`
(`LuaInterceptView::intercept_edit`), so the requested op is not where
the edit lands:
- requested outside -> intercept relocates inside: the edit stayed
hidden;
- requested inside -> intercept relocates outside: an unrelated fold
opened.
The seam still covers BOTH paths — hooking only `run_managed_edit` would
let an interactive `bypass_intercept` edit escape, which is why the
framing put it on the common entry — but each path now keys on its own
effective site:
- `run_bypass_edit` applies its op verbatim, so `run_buffer_edit`
hooks it there;
- `run_managed_edit` hooks after the intercept chain settles and
before the apply, on the op the chain returned. A chain that raises
applies nothing, so it unfolds nothing.
The registry borrow is released at that point, and the helper reads only
`SharedCore` + the fold registry, so no borrow conflicts with phase 3.
Three new tests, all through real `M-x` with a real
`pmacs.buffer.add_intercept`: relocate-into-a-fold must unfold,
relocate-out-of-a-fold must not, and a rejected chain must not. Each
asserts the buffer text first, so the test fails loudly if the intercept
stops relocating rather than silently passing.
Suite 45 -> 48.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Keep foreign BufferSnapshot publications out of existing semantic GPU
sessions while retaining grid-replica coherence. Treat dead peer writes as
peer-local failures, restore active-frontend cleanup, deterministic probe
readiness, GPU logging, shared tilde expansion, and accurate docs.
Add focused publication and cleanup coverage and record the two-window
Wayland/Vulkan smoke plus the complete post-review gate results.
All four findings were correct.
F1 (major) — the Lua widening keyed on the POINT, not the edit site.
Q#FD19 condition (iii) is `edit.range.start`; `run_buffer_edit` discarded
`op` and opened whatever fold contained `window.cursor`. Two wrong
behaviors followed: a command inserting elsewhere opened an unrelated
fold at the point, and — worse — a command editing INTO a fold from an
outside point left its edit hidden. `run_buffer_edit` now reads
`edit_start_of(&op)` before `op` is consumed and passes it down. The
`apply_active_edit` funnel stays point-keyed, which is correct and
deliberate: the six primitives, yank, and query-replace all place point
at the edit site first, and only the Lua path can diverge — now said so
in the doc comment.
The acceptance test encoded the wrong behavior (inserted at byte 0,
expected the cursor's fold to open). Replaced by two tests that pin both
directions, plus the framing's named **comment-toggle** case driven
through the real `M-;` on a file-backed Rust buffer rather than a
synthetic mutator.
F2 (major) — hidden-cursor motion normalized only the row. `move_up` /
`move_down` clamped `coord.row` but derived `goal` from the hidden
line's raw column, and returned early at a buffer boundary without
normalizing at all, so `Up` inside a fold headed on line 0 left the
logical cursor hidden. Added `normalize_cursor_to_visible`, which
projects the whole POSITION through `visible_position` as a real
mutation before any step is computed (and drops the sticky goal column,
since the jump is discontinuous). Paging shares it — the same latent
bug. The old test used equal-width lines and asserted only the line, so
it could not tell the two apart; the new fixture is deliberately ragged
and asserts the resulting BYTE in both directions, plus the boundary
case.
F3 (moderate) — `set_view_top` bypassed the fold clamp. Q#FD12/Q#FD18
say `view_top` is always set through `clamp_view_top`, naming the
`set_view_top` contract specifically. Rendering repaired it at the next
frame, but until then `view_top()` handed out a hidden line and
command/event reckoning could start from a non-visible origin. The
clamp now lives in the setter — the contract's home, what `saveplace`
and `pmacs.editor.set_view_top` call. New test reads `view_top()`
directly after the setter, before any paint.
F4 (moderate) — the suite claimed items 1–14 but omitted approved
clauses. Added: peer SELECTION endpoint projection with hidden interiors
dropped; the crossing-fold repeat for the peer cursor AND a peer
selection endpoint; a style/syntax span straddling a fold (alignment on
the shifted row); the completion popup anchored below a fold; and peer
presence in the split-buffer case using the RECIPIENT window's map.
Suite: 35 -> 45 tests. Eight new bite cases, each falsifying exactly the
line that implements its claim.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
`main` moved from c49a8c7 (folding Stage 1, #142) to 47581f4 (web
grammars, #146) while Stage 2 was in framing and implementation. The
text merge is clean, but it is NOT semantically clean: #146 added three
new `Viewport { .. }` literals to `src/highlight.rs`'s unit tests, and
Stage 2 gives `Viewport` a `folds` field. Merged alone, `cargo test
--lib` fails to compile — so the carry-over is resolved here rather
than left for CI to discover.
Merged (not rebased) so the four framing revisions the review rounds
cite by SHA (59410c0, e221f13, 8160d66, 4222ffa) stay reachable.
Full gate suite re-run on the merged tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
`cargo test --lib --features crdt` compiles two `#[cfg(feature =
"crdt")]` daemon tests that call `build_fresh_frontend_view`. Both model
a semantic session, so they pass `false` — the Q#FD21 value a
`semantic_render` attach gets until Stage 3. Bet B8 in the framing is
exactly this: the projection is chosen explicitly at every construction
site, never inferred, so a new call site cannot silently default.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Bite-verification found the Q#FD19 active-window-buffer requirement's
test vacuous: `other` held no fold, so removing the guard changed
nothing — `unfold_containing` on a store-less buffer is a no-op either
way. The guard's real job is to stop the invoking frontend's POINT from
naming a place in a buffer it is not looking at, so the fixture now
gives `other` a fold whose range contains the active window's cursor
byte. Reverting the guard now opens it (0 != 1).
Verified: with `if window.buffer_id != id { return; }` replaced by a
no-op, the test fails on a clean assertion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Implements docs/folding-stage2-framing.md rev 4. The daemon grid
renderer now consults the fold store: hidden lines are omitted, rows
below shift up, and every consumer that assumed
`display_row = source_line - view_top` routes through one shared
projection. No wire schema change and no protocol bump (Bet B6) — the
collapse is entirely daemon-side; the GPU path is Stage 3.
The spine (Q#FD12) is `src/fold_view.rs`: a `VisibleLineMap` derived
from `FoldRegistry::folds` plus a window's line offsets and never
stored. Its unit is a merged **hidden component** — overlapping OR
adjacent hidden intervals unioned, each keeping the one visible
`head_line` and that line's exact `head_position`. Adjacent intervals
merge because the later fold's head is itself hidden, which is what
makes nesting, shared heads, and crossing overlap all resolve to a
head that can actually render (round-3 F2).
Instances are short-lived and built **per rendered window** and **per
command/event operation**, never once per frame: `paint_frame` renders
several windows that may show different buffers, so a singleton would
leak one pane's folds into another (round-2 F2). The render instance
rides on a lifetime-bearing `Viewport<'a>` as `Option<&'a
VisibleLineMap>` — a shared ref is `Copy`, so `Viewport` stays `Copy`
(Bet B7).
Rendering:
- `TextView::render` walks visible lines; the head line gets a
trailing content-area ellipsis (Q#FD13).
- The gutter walks visible lines too: Absolute keeps the raw `line+1`,
Relative/Hybrid measure VISIBLE distance anchored on the cursor's
visible head (Q#FD14). The fold glyph takes the col-0 sign cell only
when a gutter exists — line numbers default to Off, so with no gutter
the ellipsis is the sole marker (Q#FD20, round-1 F3). A diagnostic
clamped onto the head wins that cell by paint order.
- A diagnostic on a hidden line clamps its SIGN to the outermost
visible head (most-severe merge); the squiggle needs a real row, so
only the sign clamps (Q#FD15).
- Caret, local selection endpoints, and peer cursors project via
`visible_position_of` — the head row AND the head's end-of-content
column, never an arbitrary column (round-2 F3). Peer presence derives
the RECIPIENT window's map.
- Style/search/completion overlays route through
`Viewport::row_offset_of`; the mode-line indicator reckons in
visible-line space.
Command/event time is scoped per frontend (Q#FD21): a
`fold_projection` flag on `FrontendView`, set at attach from the
negotiated `semantic_render` bit (grid ⇒ true, semantic ⇒ false until
Stage 3, LOCAL ⇒ true) and never inferred from a `FrontendId` (Bet
B8). Without it, shared `EditorCore` motion would make a simultaneous
unfolded GPU session's cursor skip lines it still displays. The map's
two axes stay separate (round-3 F1): the acting frontend supplies the
policy, the operation's TARGET window supplies the buffer — a wheel
event names a pane without activating it.
Motion (Q#FD17, ruled: include), paging, wheel, the click inverse, and
the auto-scroll clamp all step by visible lines under that gate;
motion from a hidden logical cursor normalizes to the visible head
first. `view_top` stays a source-line index (Bet B5), set only via
`clamp_view_top` so it never rests hidden.
Unfold widening (Q#FD19): the pre-edit unfold moves to the top of
`apply_active_edit` — one funnel that subsumes the six primitives'
calls and covers yank + query-replace, both of which place point at
the edit site first. Interactive Lua mutators hook the common
`run_buffer_edit`, above the managed/bypass split, gated on
`InteractiveCommandOrigin` AND the edit targeting that frontend's
active-window buffer. The remote/optimistic-CRDT path stays excluded
(Stage 3); undo/redo unfold stays deferred.
Acceptance: `tests/folding_stage2_acceptance.rs`, 35 tests asserting
on the real `paint_frame` cell grid, covering framing items 1–14
including crossing folds, a nested deeply-hidden cursor, a split of
two different buffers with an inactive-pane wheel, and simultaneous
grid+semantic motion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Update the framing, durable handoff, and active-work ledger after integrating
current canonical main and completing the required gates and real GPU smoke.
Add protocol-v20 semantic bootstrap and readiness result framing so
`pmacs --gpu FILE` opens the requested path before the GPU window becomes
ready. Keep target identity scoped to the authenticated frontend, preserve
legacy/no-target attach behavior, and publish fresh buffers coherently to
existing replicas.
Carry Unix path bytes and launcher cwd through the root broker, resolve paths
lexically in the daemon, reuse or create buffers without ambient-view state,
and preserve the managed daemon lifecycle from #141. Add focused parser,
wire, lifecycle, hook, isolation, and real-connector acceptance coverage.
Route command-time visible-line maps through each operation's target
window, while retaining the acting frontend as the projection-policy
owner. Model nested and crossing folds as merged hidden components so
row and byte clamps always resolve to one actually visible head.
Also key projection on the negotiated render selection and correct the
unmerged status of the separate Stage 1 housekeeping PR.
Round 2's three findings + two nits, all verified against c49a8c7:
- F1 (major): fold-aware motion must be frontend-projection scoped. Shared
EditorCore::move_up/down/page_* would make a simultaneous unfolded GPU
session skip source lines it still displays (a grid + a semantic session
can attach to one buffer, daemon.rs:876). Add a per-FrontendView
`fold_projection_active` flag (editor_core.rs:240, set at attach / cleared
at detach); gate ALL command-time visible-line reckoning (motion, paging,
wheel, click, auto-scroll) on it. Render-time clamps are already
grid-path-only. New Q#FD21 + simultaneous TUI+semantic acceptance.
- F2 (major): render maps must be per WINDOW, not per frame — paint_frame
and the presence pass iterate windows with distinct buffer_ids
(editor.rs:2922, overlay_paint.rs:124). Specify one map per rendered
nonterminal window (keyed on window buffer_id + TextView); peer presence
uses the recipient window's map. New split-of-different-buffers acceptance.
- F3 (moderate): hidden positions need COLUMN projection, not only row
clamping. Add `visible_position_of(pos)` -> outermost fold's range.start
(end of visible head line, Stage 1's point-move target) for local/peer
carets and selection endpoints; hidden interiors still drop. New
hidden-cursor-column-differs acceptance.
- Nits: fix the Viewport<'a> typo; state build cost honestly as O(folds)
with a byte->line lookup per fold (B4).
PR #147 (the #142 housekeeping) confirmed clean by the reviewer, no findings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Review found that adding ("attribute", fg(3)) for HTML/CSS also colours the
@attribute capture three already-bundled grammars emit — rust (attribute_item),
lua (<const>), yaml (directives) — which were previously unpainted. Verified on
a Rust buffer: #[derive(Debug)] now paints uniformly yellow (fg 3), an
improvement over unpainted and the distinct-attribute convention most editors
follow.
Name this retro-paint as intended in the framing (Q#WEB4, rev 4) and pin it with
rust_attribute_repaints_via_shared_attribute_capture so it is a chosen effect,
not incidental. @tag is unaffected (HTML/CSS only).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 1's five findings + two rulings, all verified against c49a8c7:
- F1 (major): nested folds. `head_of`(innermost) could clamp onto a
still-hidden inner head. Replace with `visible_head_of` (outermost
visible head); `view_top` clamps BACKWARD to the head, not forward
past the fold; relative numbers anchor on the clamped visible cursor.
- F2 (major): the consumer census was incomplete. Add the full §2.2
table — local selection (editor.rs:3241), peer presence
(overlay_paint.rs:159, after paint_frame), mode-line indicator
(editor.rs:3803), style/search/completion overlays — and make TUI
peer-presence fold behavior explicit scope.
- F3 (major): line numbers default Off => gutter_w==0 => no sign cell.
Make the fold glyph conditional: off => ellipsis only; on => sign
cell with diagnostic priority. Dedicated column (unconditional) named
as a deferred layout change.
- F4 (major): a frame-pinned map can't serve command-time motion, and
Viewport is Copy. Reframe as one derivation primitive with per-phase
short-lived instances (render via Option<&VisibleLineMap> on a
lifetime-bearing Viewport, preserving Copy; after-frame direct;
command-time fresh); home usable from EditorCore.
- F5 (moderate): key the Lua-path widening on InteractiveCommandOrigin
(editor.rs:53), hook the common run_buffer_edit (not only
run_managed_edit) so bypass_intercept edits don't escape, require the
target to be the invoking frontend's active-window buffer, and
explicitly DEFER undo/redo unfold.
Rulings: Q#FD17 include (normalize a hidden cursor to the visible head
before stepping); #142 housekeeping stays a separate docs PR.
Acceptance expanded to pin nested-fold/shared-cursor, local selection,
peer presence, an ordinary overlay across a fold, completion anchoring,
the scroll indicator, and both gutter-off/gutter-on fold-marker cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Reframe Arc 6 folding Stage 2 in detail off canonical main @ c49a8c7
(Stage 1 / #142 merged), per the parent framing's §8/§14. Continues the
Q#FD scheme from Q#FD12.
Stage 2 makes the daemon grid renderer fold-aware: collapse hidden lines,
head-line ellipsis + gutter fold glyph, fold-aware line numbers
(visible-line relative distance), diagnostic-sign clamp-to-head, caret
clamp, visible-line viewport/scroll accounting, and the interactive-Lua
unfold widening (yank/query-replace/comment). No wire schema or protocol
change — FoldState production (Stage 1) is untouched; the GPU path is
Stage 3.
Scout findings that shaped the framing:
- The TUI has NO non-identity source-line->display-row map today; the
identity `view_top + row` is baked into ~7 sites. Folding is the first
such map, so Stage 2's spine is one shared per-frame visible-line map
(Q#FD12) that the render loop and every view_top-arithmetic site
consult; collapse lives in TextView::render, not the diff shell.
- Correcting the parent's premise: yank + query-replace are
apply_active_edit callers (local), not Lua-mutator callers; only
comment-toggle/yank-pop take the Lua path (shared with the
remote/optimistic-CRDT apply that stays deferred to Stage 3). The
widening hooks the local funnels only (Q#FD19).
One open scope fork flagged for the user: Q#FD17 (fold-aware vertical
line-motion vs render-time caret clamp only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Advance the active lane to Revision 2 and record closure of all four
non-structural framing findings. Keep implementation gated on explicit user
approval.
Pin launcher-owned tilde expansion, require same-buffer dedup hooks, fail
closed when hooks kill the target, and document stderr feedback during the
pre-window bootstrap wait. Record the observed protocol-version echo and
non-Unicode argv panic.
Register `html` (.html/.htm/.xhtml) and `css` (.css) entries in
BUILTIN_LANGUAGES, backed by the official tree-sitter-html 0.23 and
tree-sitter-css 0.25 grammars over the tree-sitter-language shim (ABI-fine, no
overlay — both export their query constants). The single `extensions` field
wires detection ahead of the LSP filetype map.
HTML's crate-exported INJECTIONS_QUERY lights up <script> -> javascript
(already registered) and <style> -> css (registered here) via the #122
injection engine — the north-star injection consumer.
The only capture reconciliation (Q#WEB4): the two web captures both grammars'
queries use that pmacs did not recognize — ("tag", fg(5)) and
("attribute", fg(3)) — added to highlight.rs's table; @tag.error prefix-walks
to tag, and everything else already maps.
Tests: table guards; load-and-parse smokes (roots document/stylesheet);
highlights-resolve (node-name compat gate, asserts @tag present); extension
resolution; a tag+attribute paint test (the attribute assertion is
load-bearing); and the injection payoff — an HTML buffer with embedded
<style>/<script> paints a CSS property and a JS keyword INSIDE the injected
regions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add tree-sitter HTML and CSS grammars for .html/.htm/.xhtml and .css
highlighting, and light up HTML's <script>->javascript / <style>->css
injections (the north-star injection consumer). Both crates export their query
constants over the tree-sitter-language shim (no overlay, ABI-fine). The only
reconciliation is two new capture entries (tag, attribute) in highlight.rs.
Framing approved through three review rounds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>