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
|
/// rows form. Also gates the per-row detail lookup: a peer that
|
||||||
/// cannot carry a detail does not pay to resolve one.
|
/// cannot carry a detail does not pay to resolve one.
|
||||||
peer_knows_minibuffer_rows: bool,
|
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
|
/// Last emitted `CompletionPopup` payload per buffer (Arc 1a
|
||||||
/// Q#C5), for cached-compare suppression (see
|
/// Q#C5), for cached-compare suppression (see
|
||||||
/// [`CompletionPopupFacts`]).
|
/// [`CompletionPopupFacts`]).
|
||||||
|
|
@ -536,6 +545,8 @@ impl SemanticRenderState {
|
||||||
s.peer_knows_statusline_segments = negotiated_protocol_version >= 18;
|
s.peer_knows_statusline_segments = negotiated_protocol_version >= 18;
|
||||||
s.peer_knows_terminal_frames = negotiated_protocol_version >= 19;
|
s.peer_knows_terminal_frames = negotiated_protocol_version >= 19;
|
||||||
s.peer_knows_panel_frames = negotiated_protocol_version >= PANEL_MIN_VERSION;
|
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
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -556,6 +567,7 @@ impl SemanticRenderState {
|
||||||
last_menu_prompt: HashMap::new(),
|
last_menu_prompt: HashMap::new(),
|
||||||
last_minibuffer: None,
|
last_minibuffer: None,
|
||||||
peer_knows_minibuffer_rows: true,
|
peer_knows_minibuffer_rows: true,
|
||||||
|
peer_knows_mapped_panel: false,
|
||||||
last_completion_popup: HashMap::new(),
|
last_completion_popup: HashMap::new(),
|
||||||
last_summary: HashMap::new(),
|
last_summary: HashMap::new(),
|
||||||
last_status: HashMap::new(),
|
last_status: HashMap::new(),
|
||||||
|
|
@ -1436,7 +1448,7 @@ impl SemanticRenderState {
|
||||||
self.publish_absent_panel(out);
|
self.publish_absent_panel(out);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let payload = PanelFramePayload::Present(PanelFrame {
|
let frame = PanelFrame {
|
||||||
buffer_id: projection.buffer_id,
|
buffer_id: projection.buffer_id,
|
||||||
panel_epoch,
|
panel_epoch,
|
||||||
geometry_epoch: geometry.geometry_epoch,
|
geometry_epoch: geometry.geometry_epoch,
|
||||||
|
|
@ -1444,7 +1456,28 @@ impl SemanticRenderState {
|
||||||
cells: projection.cells,
|
cells: projection.cells,
|
||||||
cursor: projection.cursor,
|
cursor: projection.cursor,
|
||||||
focused: projection.focused,
|
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
|
// Complete-payload comparison FIRST, like the terminal pass: only
|
||||||
// validated payloads are ever stored, so a payload equal to the
|
// validated payloads are ever stored, so a payload equal to the
|
||||||
// baseline has already passed and re-running the per-cell width
|
// baseline has already passed and re-running the per-cell width
|
||||||
|
|
@ -1453,8 +1486,10 @@ impl SemanticRenderState {
|
||||||
self.panel_error_latched = false;
|
self.panel_error_latched = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let PanelFramePayload::Present(frame) = &payload else {
|
let (PanelFramePayload::Present(frame) | PanelFramePayload::PresentMapped { frame, .. }) =
|
||||||
unreachable!("the Present payload was constructed immediately above");
|
&payload
|
||||||
|
else {
|
||||||
|
unreachable!("a Present payload was constructed immediately above");
|
||||||
};
|
};
|
||||||
match frame.validate() {
|
match frame.validate() {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
|
|
||||||
|
|
@ -117,9 +117,16 @@ impl Session {
|
||||||
first
|
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 {
|
fn present(&mut self) -> PanelFrame {
|
||||||
match self.frame() {
|
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:?}"),
|
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);
|
session.declare(1, ROWS, COLS);
|
||||||
open_panel(&session, "*panel*", 4);
|
open_panel(&session, "*panel*", 4);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(session.frame(), Some(PanelFramePayload::Present(_))),
|
matches!(
|
||||||
|
session.frame(),
|
||||||
|
Some(PanelFramePayload::Present(_) | PanelFramePayload::PresentMapped { .. })
|
||||||
|
),
|
||||||
"{label}: fixture precondition — a band was visible first"
|
"{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
|
let panel_rows = messages
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|message| match message {
|
.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,
|
_ => None,
|
||||||
})
|
})
|
||||||
.expect("a panel frame");
|
.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);
|
session.state.core.borrow_mut().focus_window(FID, panel);
|
||||||
|
|
||||||
let frame = match session.frame() {
|
let frame = match session.frame() {
|
||||||
Some(PanelFramePayload::Present(frame)) => frame,
|
Some(
|
||||||
|
PanelFramePayload::Present(frame) | PanelFramePayload::PresentMapped { frame, .. },
|
||||||
|
) => frame,
|
||||||
other => panic!(
|
other => panic!(
|
||||||
"a legally wide panel must still present its terminal; got {other:?} \
|
"a legally wide panel must still present its terminal; got {other:?} \
|
||||||
— and the durable state says hidden={}",
|
— and the durable state says hidden={}",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue