Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/app/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,11 @@ pub(super) fn restored_command(argv: &[String], session: Option<(&str, &str)>, l
(!live).then(|| m.resume_argv(id)).flatten().or(stored).or_else(|| Some(vec![m.program().to_string()]))
}

/// What a restarted pane types: its session resumed, else the agent it ran started fresh; a plain shell types nothing.
pub(super) fn restart_command(argv: &[String], session: Option<(&str, &str)>, agent: Option<&str>) -> Option<Vec<String>> {
restored_command(argv, session, false).or_else(|| agent.and_then(crate::agents::get).map(|m| vec![m.program().to_string()]))
}

/// The line typed into a restored pane's shell; None when an argument holds a control character.
pub(super) fn restore_line(cmd: &[String]) -> Option<String> {
let words = cmd.iter().map(|a| shell_word(a)).collect::<Option<Vec<_>>>()?;
Expand Down
16 changes: 16 additions & 0 deletions src/app/mail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1383,6 +1383,22 @@ mod tests {
assert_eq!(app.pane_by_name("planner"), Some(new));
}

#[test]
fn a_restarted_pane_starts_with_the_old_programs_modes_off() {
let mut app = app();
let old = &mut app.pane_mut(1).unwrap().term;
old.feed(b"kept above\r\n\x1b[?1049h\x1b[?1003h\x1b[?1006h\x1b[?2004h\x1b[?1h\x1b[?25l");
assert!(old.wants_mouse());
let new = app.alloc_id();
let pty = crate::app::snapshots::agent_pane(&app, new, crate::term::AgentState::Idle, 0).pty;
assert!(app.restarted(1, new, pty, None, "zsh".into()));
let term = &mut app.pane_mut(new).unwrap().term;
let s = term.screen();
assert!(!term.wants_mouse(), "a mouse move would be typed into the shell");
assert!(!s.alternate_screen() && !s.bracketed_paste() && !s.app_cursor() && !s.hide_cursor());
assert!(term.all_lines().iter().any(|l| l == "kept above"), "the text stays");
}

fn req(to: &str, text: &str) -> SendReq {
SendReq { to: to.into(), text: text.into(), reply_to: None, auto: false, ask_route: false }
}
Expand Down
11 changes: 11 additions & 0 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3686,6 +3686,17 @@ mod tests {
assert_eq!(super::restored_command(&gem, None, false), Some(vec!["gemini".into()]), "its own flags stripped");
}

#[test]
fn a_restarted_agent_starts_again_without_a_session() {
let zsh = vec!["/bin/zsh".to_string(), "-l".to_string()];
assert_eq!(super::restart_command(&zsh, None, Some("pi")), Some(vec!["pi".into()]), "the agent it ran, started fresh");
assert_eq!(super::restart_command(&zsh, None, None), None, "a plain shell types nothing");
assert_eq!(super::restart_command(&zsh, None, Some("gone")), None, "an agent with no manifest: the shell only");
assert_eq!(super::restart_command(&zsh, Some(("pi", "019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b")), Some("pi")), Some(vec!["pi".into(), "--session".into(), "019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b".into()]), "its session resumed");
let gem = vec!["gemini".to_string(), "--resume".to_string(), "x".to_string()];
assert_eq!(super::restart_command(&gem, None, None), Some(vec!["gemini".into()]), "a pane started as the agent");
}

#[test]
fn an_option_letter_means_one_thing_everywhere() {
// The tree answers `option` letters of its own; none of them may be a global one, or
Expand Down
9 changes: 6 additions & 3 deletions src/app/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,8 @@ impl App {
// The old pty's Drop asks the broker to kill this id, and the broker keys panes by id
// alone: it has to be gone before the new shell exists, or the kill lands on the shell
// we just started. That is how the first attempt at this closed the tab anyway.
let Pane { term, cwd, label, isolation, pty, .. } = old;
let Pane { mut term, cwd, label, isolation, pty, .. } = old;
term.modes_off();
let (cols, rows) = (pty.cols, pty.rows);
drop(pty);
let pty = match Pty::spawn(id, &shell_argv(), &cwd, cols, rows, bell, &isolation) {
Expand All @@ -292,6 +293,7 @@ impl App {
return false;
}
let ps = pane_snapshot(old);
let agent = old.agent.clone();
let new = self.alloc_id();
let mut pty = match Pty::spawn(new, &shell_argv(), &ps.cwd, ps.cols, ps.rows, self.bell.clone(), &ps.isolation) {
Ok(pty) => pty,
Expand All @@ -302,7 +304,7 @@ impl App {
};
// Nobody else holds the session: nerd just stopped its holder.
let session = ps.session().filter(|(a, _)| crate::agents::get(a).as_ref().is_some_and(crate::hook::resume_confirmed));
let launch = session.as_ref().and_then(|(a, sid)| super::lifecycle::restored_command(&ps.argv, Some((a, sid)), false));
let launch = super::lifecycle::restart_command(&ps.argv, session.as_ref().map(|(a, sid)| (a.as_str(), sid.as_str())), agent.as_deref());
if let Some(line) = launch.as_deref().and_then(super::lifecycle::restore_line) {
pty.write(line.as_bytes());
}
Expand All @@ -315,7 +317,8 @@ impl App {
let Some((si, ti)) = self.locate(id) else { return false };
let Some(old) = self.spaces[si].tabs[ti].panes.remove(&id) else { return false };
// The old pty drops here: the broker kills its program.
let Pane { term, cwd, label, isolation, .. } = old;
let Pane { mut term, cwd, label, isolation, .. } = old;
term.modes_off();
let pane = Pane { label, isolation, command, agent_session, ..Pane::new(new, term, pty, cwd, shell_argv()) };
let tab = &mut self.spaces[si].tabs[ti];
tab.panes.insert(new, pane);
Expand Down
8 changes: 8 additions & 0 deletions src/term.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,14 @@ impl Term {
self.advance(b"\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1005l\x1b[?1006l");
}

/// Input modes and the alternate screen as a fresh terminal has them, text kept: for the next program in a pane.
pub fn modes_off(&mut self) {
self.mouse_off();
self.advance(b"\x1b[?1049l\x1b[?1l\x1b>\x1b[?2004l\x1b[?1004l\x1b[=0u\x1b[?25h\x1b[?7h\x1b[m");
self.theme_notify = false;
self.pointer = None;
}

/// Whether the program inside the pane is requesting mouse events.
pub fn wants_mouse(&self) -> bool {
self.screen().mouse_protocol_mode() != MouseProtocolMode::None
Expand Down
Loading