feat(panel): outbound family selection (G6a/G7a half)
The producer half of bilateral gating. Inbound routing, the four refusal quadrants and the positive routing controls are the next commit; the full eleven-stage `--protocol` gate runs at that checkpoint, once all of them discriminate. `SemanticRenderState` gains `peer_knows_mapped_panel`, set in `for_peer` beside the six existing capability bools --- the pattern this codebase already uses to bake a negotiated version into the producer. A `>= v25` peer receives `PresentMapped`; every older peer receives legacy `Present`, unchanged. **THE ORDERING IS LOAD-BEARING AND IS WRITTEN DOWN AT THE SITE.** Projection first, THEN capture-and-advance the mapping key, THEN construct the payload. A terminal projection registers the view whose scroll anchor the key reads, so capturing earlier stamps a frame with a key derived from an anchor that does not yet exist. One case is deliberately not a fallback: if a `>= v25` peer has no presentable mapping to stamp, the producer publishes `Absent` rather than dropping to legacy `Present`. Falling back would hand a peer the family it did not negotiate --- the bypass the gate exists to close, arriving from the daemon's own side. **Thirty existing rows broke, and that is the gate working.** The fixture negotiates `PROTOCOL_VERSION`, so every panel test suddenly received the mapped family where it matched `Present` literally. Those rows are about the PROJECTION, not the wrapper, so the fixture helpers are family-agnostic now and which family carries a frame is pinned by the G6/G7/G8 rows rather than incidentally by thirty others. Verified: focused suite 37/37, `cargo test --lib` 1945 green, clippy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
parent
921989a77c
commit
7da2f34b94
|
|
@ -254,6 +254,15 @@ pub struct SemanticRenderState {
|
|||
/// rows form. Also gates the per-row detail lookup: a peer that
|
||||
/// cannot carry a detail does not pay to resolve one.
|
||||
peer_knows_minibuffer_rows: bool,
|
||||
/// §5b — whether the peer negotiated `>= v25` and therefore takes
|
||||
/// the **mapped** panel family.
|
||||
///
|
||||
/// The families are exclusive in both directions: a `>= v25` peer
|
||||
/// receives `PresentMapped` and never legacy `Present`, and a
|
||||
/// `<= v24` peer the reverse. "Send whichever and let the receiver
|
||||
/// cope" would make negotiation a sender convention rather than a
|
||||
/// gate.
|
||||
peer_knows_mapped_panel: bool,
|
||||
/// Last emitted `CompletionPopup` payload per buffer (Arc 1a
|
||||
/// Q#C5), for cached-compare suppression (see
|
||||
/// [`CompletionPopupFacts`]).
|
||||
|
|
@ -536,6 +545,8 @@ impl SemanticRenderState {
|
|||
s.peer_knows_statusline_segments = negotiated_protocol_version >= 18;
|
||||
s.peer_knows_terminal_frames = negotiated_protocol_version >= 19;
|
||||
s.peer_knows_panel_frames = negotiated_protocol_version >= PANEL_MIN_VERSION;
|
||||
s.peer_knows_mapped_panel =
|
||||
negotiated_protocol_version >= pmacs_protocol::PANEL_MAPPING_MIN_VERSION;
|
||||
s
|
||||
}
|
||||
|
||||
|
|
@ -556,6 +567,7 @@ impl SemanticRenderState {
|
|||
last_menu_prompt: HashMap::new(),
|
||||
last_minibuffer: None,
|
||||
peer_knows_minibuffer_rows: true,
|
||||
peer_knows_mapped_panel: false,
|
||||
last_completion_popup: HashMap::new(),
|
||||
last_summary: HashMap::new(),
|
||||
last_status: HashMap::new(),
|
||||
|
|
@ -1436,7 +1448,7 @@ impl SemanticRenderState {
|
|||
self.publish_absent_panel(out);
|
||||
return;
|
||||
};
|
||||
let payload = PanelFramePayload::Present(PanelFrame {
|
||||
let frame = PanelFrame {
|
||||
buffer_id: projection.buffer_id,
|
||||
panel_epoch,
|
||||
geometry_epoch: geometry.geometry_epoch,
|
||||
|
|
@ -1444,7 +1456,28 @@ impl SemanticRenderState {
|
|||
cells: projection.cells,
|
||||
cursor: projection.cursor,
|
||||
focused: projection.focused,
|
||||
});
|
||||
};
|
||||
// §5b — ORDER MATTERS. The projection is prepared above, THEN
|
||||
// the key is captured, THEN the payload is built. A terminal
|
||||
// projection registers the view whose scroll anchor the key
|
||||
// reads, so capturing earlier would stamp a frame with a key
|
||||
// derived from an unregistered anchor.
|
||||
let payload = if self.peer_knows_mapped_panel {
|
||||
let snapshot = state.panel_mapping_snapshot(self.frontend_id);
|
||||
let Some(mapping_generation) = self.panel_mapping_generation(snapshot) else {
|
||||
// No presentable mapping means nothing to stamp. Falling
|
||||
// back to the legacy variant here would hand a v25 peer
|
||||
// the family it did not negotiate.
|
||||
self.publish_absent_panel(out);
|
||||
return;
|
||||
};
|
||||
PanelFramePayload::PresentMapped {
|
||||
frame,
|
||||
mapping_generation,
|
||||
}
|
||||
} else {
|
||||
PanelFramePayload::Present(frame)
|
||||
};
|
||||
// Complete-payload comparison FIRST, like the terminal pass: only
|
||||
// validated payloads are ever stored, so a payload equal to the
|
||||
// baseline has already passed and re-running the per-cell width
|
||||
|
|
@ -1453,8 +1486,10 @@ impl SemanticRenderState {
|
|||
self.panel_error_latched = false;
|
||||
return;
|
||||
}
|
||||
let PanelFramePayload::Present(frame) = &payload else {
|
||||
unreachable!("the Present payload was constructed immediately above");
|
||||
let (PanelFramePayload::Present(frame) | PanelFramePayload::PresentMapped { frame, .. }) =
|
||||
&payload
|
||||
else {
|
||||
unreachable!("a Present payload was constructed immediately above");
|
||||
};
|
||||
match frame.validate() {
|
||||
Ok(()) => {
|
||||
|
|
|
|||
|
|
@ -117,9 +117,16 @@ impl Session {
|
|||
first
|
||||
}
|
||||
|
||||
/// The frame from whichever Present family this session negotiated.
|
||||
///
|
||||
/// Family-agnostic on purpose: these rows are about the PROJECTION,
|
||||
/// and which wrapper carries it is §5b's own concern, pinned by the
|
||||
/// G6/G7/G8 rows rather than incidentally by thirty others.
|
||||
fn present(&mut self) -> PanelFrame {
|
||||
match self.frame() {
|
||||
Some(PanelFramePayload::Present(frame)) => frame,
|
||||
Some(
|
||||
PanelFramePayload::Present(frame) | PanelFramePayload::PresentMapped { frame, .. },
|
||||
) => frame,
|
||||
other => panic!("expected a Present panel payload, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -522,7 +529,10 @@ fn acc41_degenerate_geometry_fails_closed_to_zero_usable_grid() {
|
|||
session.declare(1, ROWS, COLS);
|
||||
open_panel(&session, "*panel*", 4);
|
||||
assert!(
|
||||
matches!(session.frame(), Some(PanelFramePayload::Present(_))),
|
||||
matches!(
|
||||
session.frame(),
|
||||
Some(PanelFramePayload::Present(_) | PanelFramePayload::PresentMapped { .. })
|
||||
),
|
||||
"{label}: fixture precondition — a band was visible first"
|
||||
);
|
||||
|
||||
|
|
@ -898,7 +908,9 @@ fn acc45_one_statusline_invocation_serves_the_document_and_the_panel() {
|
|||
let panel_rows = messages
|
||||
.iter()
|
||||
.find_map(|message| match message {
|
||||
InstanceMessage::PanelFrame(PanelFramePayload::Present(frame)) => Some(rows_of(frame)),
|
||||
InstanceMessage::PanelFrame(
|
||||
PanelFramePayload::Present(frame) | PanelFramePayload::PresentMapped { frame, .. },
|
||||
) => Some(rows_of(frame)),
|
||||
_ => None,
|
||||
})
|
||||
.expect("a panel frame");
|
||||
|
|
@ -1327,7 +1339,9 @@ fn sweep_a_panel_wider_than_the_terminal_cap_still_presents_its_terminal() {
|
|||
session.state.core.borrow_mut().focus_window(FID, panel);
|
||||
|
||||
let frame = match session.frame() {
|
||||
Some(PanelFramePayload::Present(frame)) => frame,
|
||||
Some(
|
||||
PanelFramePayload::Present(frame) | PanelFramePayload::PresentMapped { frame, .. },
|
||||
) => frame,
|
||||
other => panic!(
|
||||
"a legally wide panel must still present its terminal; got {other:?} \
|
||||
— and the durable state says hidden={}",
|
||||
|
|
|
|||
Loading…
Reference in New Issue