diff --git a/src/app/lifecycle.rs b/src/app/lifecycle.rs index 8ed63ef..718ba88 100644 --- a/src/app/lifecycle.rs +++ b/src/app/lifecycle.rs @@ -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> { + 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 { let words = cmd.iter().map(|a| shell_word(a)).collect::>>()?; diff --git a/src/app/mail.rs b/src/app/mail.rs index da568ac..da510c4 100644 --- a/src/app/mail.rs +++ b/src/app/mail.rs @@ -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 } } diff --git a/src/app/mod.rs b/src/app/mod.rs index d376313..6f4e296 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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 diff --git a/src/app/model.rs b/src/app/model.rs index 02d3b5f..ae98f4c 100644 --- a/src/app/model.rs +++ b/src/app/model.rs @@ -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) { @@ -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, @@ -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()); } @@ -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); diff --git a/src/term.rs b/src/term.rs index 208738e..d32d73a 100644 --- a/src/term.rs +++ b/src/term.rs @@ -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