mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 00:15:06 +00:00
fix(daemon): a pane keeps its private modes across a handoff (#1145)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2afebab175
commit
a39d97d5c5
@@ -94,6 +94,8 @@ struct PaneRecord {
|
||||
agent: Option<crate::core::cli_agent::CLIAgent>,
|
||||
agent_argv: Option<Vec<String>>,
|
||||
agent_session: Option<crate::core::cli_agent::AgentSessionState>,
|
||||
#[serde(default)]
|
||||
head_modes: Option<Vec<u8>>,
|
||||
/// Length of this pane's ring in the data section, which follows the
|
||||
/// manifest in pane order.
|
||||
ring_len: u32,
|
||||
@@ -242,6 +244,7 @@ fn stage(panes: &[Carried], next_pane_id: u64) -> std::io::Result<std::fs::File>
|
||||
agent: pane.agent,
|
||||
agent_argv: pane.agent_argv.clone(),
|
||||
agent_session: pane.agent_session.clone(),
|
||||
head_modes: pane.head_modes.clone(),
|
||||
ring_len: encoded.len() as u32,
|
||||
});
|
||||
data.extend_from_slice(&encoded);
|
||||
@@ -376,6 +379,7 @@ pub fn adopt(fd: RawFd) -> Option<Adopted> {
|
||||
agent: record.agent,
|
||||
agent_argv: record.agent_argv,
|
||||
agent_session: record.agent_session,
|
||||
head_modes: record.head_modes,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -420,6 +424,7 @@ mod tests {
|
||||
agent: None,
|
||||
agent_argv: None,
|
||||
agent_session: None,
|
||||
head_modes: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,6 +458,18 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pane_keeps_the_modes_its_ring_no_longer_holds() {
|
||||
let mut tui = carried(5, 21, b"long after startup");
|
||||
tui.head_modes = Some(b"\x1b[>1u\x1b[?2031h".to_vec());
|
||||
let staged = stage(&[tui], 6).expect("stage");
|
||||
let adopted = adopt(std::os::fd::IntoRawFd::into_raw_fd(staged)).expect("read back");
|
||||
assert_eq!(
|
||||
adopted.panes[0].head_modes.as_deref(),
|
||||
Some(&b"\x1b[>1u\x1b[?2031h"[..])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pane_that_printed_nothing_still_crosses() {
|
||||
let mut empty = carried(3, 20, b"");
|
||||
|
||||
@@ -1420,6 +1420,11 @@ pub struct Carried {
|
||||
pub agent: Option<crate::core::cli_agent::CLIAgent>,
|
||||
pub agent_argv: Option<Vec<String>>,
|
||||
pub agent_session: Option<crate::core::cli_agent::AgentSessionState>,
|
||||
/// The modes the ring dropped off its front, as bytes that switch them
|
||||
/// back on. A program sets them once at startup, so after an `exec` a
|
||||
/// long-running TUI would otherwise lose its alternate screen, mouse, 2031
|
||||
/// and kitty keyboard flags for good.
|
||||
pub head_modes: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// A pty master this process inherited from its own previous image.
|
||||
@@ -2005,6 +2010,7 @@ impl DaemonPane {
|
||||
agent: st.agent,
|
||||
agent_argv: st.agent_argv.clone(),
|
||||
agent_session: st.agent_session.clone(),
|
||||
head_modes: st.ring.head_modes.restore_bytes(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2037,7 +2043,8 @@ impl DaemonPane {
|
||||
})
|
||||
.unwrap_or(carried.size);
|
||||
|
||||
let mut ring = ReplayRing::seeded(carried.ring, size);
|
||||
let (mut ring, modes) =
|
||||
ReplayRing::adopted(carried.ring, size, carried.head_modes.as_deref());
|
||||
// Not a restore banner: nothing was lost, so there is nothing to
|
||||
// announce and nothing to reset. The shell below these bytes is the
|
||||
// same shell that wrote them.
|
||||
@@ -2080,7 +2087,7 @@ impl DaemonPane {
|
||||
},
|
||||
remote_prompt_seen: false,
|
||||
ssh_phase: None,
|
||||
modes: TerminalModes::default(),
|
||||
modes,
|
||||
remote: carried.remote,
|
||||
agent: carried.agent,
|
||||
agent_session: carried.agent_session,
|
||||
@@ -3139,6 +3146,22 @@ impl ReplayRing {
|
||||
ring
|
||||
}
|
||||
|
||||
/// A ring carried across a handoff, with the pane's modes folded back
|
||||
/// from it: `head` is the carried [`Self::head_modes`], and the pane's
|
||||
/// modes are that plus every byte the ring still holds.
|
||||
fn adopted(
|
||||
segments: Vec<crate::daemon::scrollback::Segment>,
|
||||
size: WinSize,
|
||||
head: Option<&[u8]>,
|
||||
) -> (Self, TerminalModes) {
|
||||
let mut ring = Self::seeded(segments, size);
|
||||
if let Some(bytes) = head {
|
||||
ring.head_modes.feed(bytes);
|
||||
}
|
||||
let modes = ring.modes_from(ring.head_modes.clone());
|
||||
(ring, modes)
|
||||
}
|
||||
|
||||
/// The geometry new output would be recorded at.
|
||||
fn tail_size(&self) -> WinSize {
|
||||
self.segments
|
||||
@@ -3269,7 +3292,11 @@ impl ReplayRing {
|
||||
/// [`Self::evict`]), and the answer here has to be the one the client's
|
||||
/// emulator will reach from what is left.
|
||||
fn modes(&self) -> TerminalModes {
|
||||
let mut modes = TerminalModes::new();
|
||||
self.modes_from(TerminalModes::new())
|
||||
}
|
||||
|
||||
/// `start` with every byte the ring holds folded into it.
|
||||
fn modes_from(&self, mut modes: TerminalModes) -> TerminalModes {
|
||||
for seg in &self.segments {
|
||||
// Both halves of the deque, in order: `feed` carries a sequence
|
||||
// across calls, so the split is invisible to the fold.
|
||||
@@ -5149,6 +5176,33 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_handoff_keeps_the_modes_the_ring_dropped_off_its_front() {
|
||||
use crate::daemon::scrollback::Segment;
|
||||
|
||||
let mut head = TerminalModes::new();
|
||||
head.feed(b"\x1b[?1049h\x1b[?2031h\x1b[>1u");
|
||||
let (ring, modes) = ReplayRing::adopted(
|
||||
vec![Segment {
|
||||
size: ws(80, 24),
|
||||
bytes: b"long after startup".to_vec(),
|
||||
}],
|
||||
ws(80, 24),
|
||||
head.restore_bytes().as_deref(),
|
||||
);
|
||||
|
||||
assert!(modes.is_on(crate::core::term_modes::COLOR_SCHEME_UPDATES));
|
||||
let prelude = modes
|
||||
.replay_prelude(&ring.head_modes, &ring.modes())
|
||||
.expect("a prelude");
|
||||
let has = |needle: &[u8]| prelude.windows(needle.len()).any(|w| w == needle);
|
||||
assert!(has(b"\x1b[?2031h"));
|
||||
assert!(
|
||||
has(b"\x1b[>1u"),
|
||||
"the keyboard flags come back from the head fold, so it has to cross the exec too"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_seed_leaves_an_ordinary_ring() {
|
||||
let mut ring = ReplayRing::seeded(Vec::new(), ws(80, 24));
|
||||
|
||||
Reference in New Issue
Block a user