diff --git a/src/process/launch.rs b/src/process/launch.rs index 4008987..4c88bb1 100644 --- a/src/process/launch.rs +++ b/src/process/launch.rs @@ -88,9 +88,15 @@ pub fn launch_claude( "launching claude" ); + let resume_hint_override = resume_hint_override(); + // PTY mode (Unix only): 非交互模式跳过 PTY #[cfg(unix)] - let use_pty = !is_noninteractive && should_use_pty(&config.hyperlinks, hyperlinks_override); + let use_pty = should_launch_with_pty( + is_noninteractive, + should_use_pty(&config.hyperlinks, hyperlinks_override), + resume_hint_override.is_some(), + ); #[cfg(not(unix))] let use_pty = false; @@ -101,7 +107,8 @@ pub fn launch_claude( { tracing::info!("hyperlinks enabled, using PTY proxy mode"); let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/")); - resume_session_id = terminal::pty::spawn_with_pty(cmd, cwd)?; + resume_session_id = + terminal::pty::spawn_with_pty(cmd, cwd, resume_hint_override.is_some())?; } } else { let mut child = cmd.spawn().context("failed to execute claude binary")?; @@ -133,20 +140,44 @@ pub fn launch_claude( // 追加 claudex resume 命令提示 if let Some(session_id) = resume_session_id { - print_claudex_resume_hint(&profile.name, &session_id, extra_args); + print_claudex_resume_hint( + &profile.name, + &session_id, + extra_args, + resume_hint_override.as_deref(), + ); } Ok(()) } /// 在 Claude Code 退出后追加 claudex resume 命令提示 -fn print_claudex_resume_hint(profile_name: &str, session_id: &str, extra_args: &[String]) { - let hint = build_resume_hint(profile_name, session_id, extra_args); - eprintln!("\nResume this session with claudex:\n {hint}"); +fn print_claudex_resume_hint( + profile_name: &str, + session_id: &str, + extra_args: &[String], + override_hint: Option<&str>, +) { + let hint = build_resume_hint(profile_name, session_id, extra_args, override_hint); + let owner = if override_hint.is_some() { + "" + } else { + " claudex" + }; + eprintln!("\nResume this session with{owner}:\n {hint}"); } /// 构造 claudex resume 命令字符串(纯函数,便于测试) -fn build_resume_hint(profile_name: &str, session_id: &str, extra_args: &[String]) -> String { +fn build_resume_hint( + profile_name: &str, + session_id: &str, + extra_args: &[String], + override_hint: Option<&str>, +) -> String { + if let Some(hint) = override_hint { + return hint.to_string(); + } + // 过滤掉原始 extra_args 中的 --resume 及其值参数 let mut args_clean: Vec<&str> = Vec::new(); let mut skip_next = false; @@ -171,8 +202,27 @@ fn build_resume_hint(profile_name: &str, session_id: &str, extra_args: &[String] format!("claudex run {profile_name} --resume {session_id}{args_str}") } +fn resume_hint_override() -> Option { + let value = std::env::var("CLAUDEX_RESUME_HINT").ok()?; + let hint = value.trim(); + if hint.is_empty() || hint.len() > 1024 || hint.chars().any(char::is_control) { + return None; + } + Some(hint.to_string()) +} + /// Decide whether to use PTY mode based on config + CLI flag. #[cfg(unix)] +fn should_launch_with_pty( + is_noninteractive: bool, + hyperlinks_enabled: bool, + has_owned_resume_hint: bool, +) -> bool { + !is_noninteractive && (hyperlinks_enabled || has_owned_resume_hint) +} + +/// Decide whether hyperlinks require PTY mode based on config + CLI flag. +#[cfg(unix)] fn should_use_pty(config_hyperlinks: &HyperlinksConfig, cli_override: bool) -> bool { if cli_override { return true; @@ -191,7 +241,7 @@ mod tests { #[test] fn test_build_resume_hint_no_extra_args() { - let hint = build_resume_hint("codex-sub", "abc-123", &[]); + let hint = build_resume_hint("codex-sub", "abc-123", &[], None); assert_eq!(hint, "claudex run codex-sub --resume abc-123"); } @@ -201,7 +251,7 @@ mod tests { "--dangerously-skip-permissions".to_string(), "--verbose".to_string(), ]; - let hint = build_resume_hint("codex-sub", "abc-123", &args); + let hint = build_resume_hint("codex-sub", "abc-123", &args, None); assert_eq!( hint, "claudex run codex-sub --resume abc-123 --dangerously-skip-permissions --verbose" @@ -215,7 +265,7 @@ mod tests { "old-session-id".to_string(), "--dangerously-skip-permissions".to_string(), ]; - let hint = build_resume_hint("codex-sub", "new-session-id", &args); + let hint = build_resume_hint("codex-sub", "new-session-id", &args, None); assert_eq!( hint, "claudex run codex-sub --resume new-session-id --dangerously-skip-permissions" @@ -229,14 +279,32 @@ mod tests { "--resume".to_string(), "old-id".to_string(), ]; - let hint = build_resume_hint("my-profile", "new-id", &args); + let hint = build_resume_hint("my-profile", "new-id", &args, None); assert_eq!(hint, "claudex run my-profile --resume new-id --verbose"); } #[test] fn test_build_resume_hint_resume_only() { let args = vec!["--resume".to_string(), "old-id".to_string()]; - let hint = build_resume_hint("p", "new-id", &args); + let hint = build_resume_hint("p", "new-id", &args, None); assert_eq!(hint, "claudex run p --resume new-id"); } + + #[test] + fn test_build_resume_hint_prefers_owned_override() { + let hint = build_resume_hint( + "codex-sub", + "abc-123", + &[], + Some("orichum resume oc-s-0000000000000001"), + ); + assert_eq!(hint, "orichum resume oc-s-0000000000000001"); + } + + #[cfg(unix)] + #[test] + fn test_owned_resume_hint_forces_pty_without_hyperlinks() { + assert!(should_launch_with_pty(false, false, true)); + assert!(!should_launch_with_pty(true, false, true)); + } } diff --git a/src/terminal/pty.rs b/src/terminal/pty.rs index b3bdb2f..5694665 100644 --- a/src/terminal/pty.rs +++ b/src/terminal/pty.rs @@ -14,7 +14,11 @@ use super::osc8::LinkDetector; /// Launch a child process in a PTY and proxy its output through the link detector. /// Returns the detected resume session ID if Claude Code output a `claude --resume` line. -pub fn spawn_with_pty(mut cmd: Command, cwd: PathBuf) -> Result> { +pub fn spawn_with_pty( + mut cmd: Command, + cwd: PathBuf, + suppress_resume_footer: bool, +) -> Result> { // Open a PTY pair let pty = openpty(None, None).context("failed to open PTY")?; let master_fd = pty.master; @@ -73,8 +77,12 @@ pub fn spawn_with_pty(mut cmd: Command, cwd: PathBuf) -> Result> setup_sigwinch_handler(stdin.as_raw_fd(), master_fd.as_raw_fd()); // Run the proxy loop - let (exit_code, resume_session_id) = - run_proxy_loop(&master_fd, &stdin, &mut LinkDetector::new(cwd)); + let (exit_code, resume_session_id) = run_proxy_loop( + &master_fd, + &stdin, + &mut LinkDetector::new(cwd), + suppress_resume_footer, + ); // Restore terminal settings if let Some(ref orig) = orig_termios { @@ -107,9 +115,16 @@ fn run_proxy_loop( master_fd: &OwnedFd, stdin_handle: &std::io::Stdin, detector: &mut LinkDetector, + suppress_resume_footer: bool, ) -> (Result<()>, Option) { let mut resume_session_id: Option = None; - let result = run_proxy_loop_inner(master_fd, stdin_handle, detector, &mut resume_session_id); + let result = run_proxy_loop_inner( + master_fd, + stdin_handle, + detector, + &mut resume_session_id, + suppress_resume_footer, + ); (result, resume_session_id) } @@ -119,6 +134,7 @@ fn run_proxy_loop_inner( stdin_handle: &std::io::Stdin, detector: &mut LinkDetector, resume_session_id: &mut Option, + suppress_resume_footer: bool, ) -> Result<()> { let stdin_borrowed: BorrowedFd = stdin_handle.as_fd(); let master_borrowed: BorrowedFd = master_fd.as_fd(); @@ -139,8 +155,10 @@ fn run_proxy_loop_inner( Ok(0) => { // Timeout: flush any incomplete line buffer to avoid display lag if !line_buf.is_empty() { - let enhanced = detector.enhance_line(&line_buf); - write!(stdout, "{enhanced}")?; + if !should_suppress_resume_footer_line(&line_buf, suppress_resume_footer) { + let enhanced = detector.enhance_line(&line_buf); + write!(stdout, "{enhanced}")?; + } stdout.flush()?; line_buf.clear(); } @@ -199,8 +217,10 @@ fn run_proxy_loop_inner( // 检测 `claude --resume ` detect_resume_session(&line, resume_session_id); - let enhanced = detector.enhance_line(&line); - writeln!(stdout, "{enhanced}")?; + if !should_suppress_resume_footer_line(&line, suppress_resume_footer) { + let enhanced = detector.enhance_line(&line); + writeln!(stdout, "{enhanced}")?; + } } stdout.flush()?; @@ -214,8 +234,10 @@ fn run_proxy_loop_inner( // Child exited: flush remaining buffer if !line_buf.is_empty() { detect_resume_session(&line_buf, resume_session_id); - let enhanced = detector.enhance_line(&line_buf); - write!(stdout, "{enhanced}")?; + if !should_suppress_resume_footer_line(&line_buf, suppress_resume_footer) { + let enhanced = detector.enhance_line(&line_buf); + write!(stdout, "{enhanced}")?; + } stdout.flush()?; } break; @@ -239,6 +261,15 @@ fn detect_resume_session(line: &str, session_id: &mut Option) { } } +fn should_suppress_resume_footer_line(line: &str, enabled: bool) -> bool { + if !enabled { + return false; + } + let stripped = strip_ansi_escapes(line); + let trimmed = stripped.trim(); + trimmed == "Resume this session with:" || trimmed.starts_with("claude --resume ") +} + /// 剥离 ANSI 转义序列(CSI、OSC 等) fn strip_ansi_escapes(s: &str) -> String { let mut out = String::with_capacity(s.len()); @@ -504,4 +535,20 @@ mod tests { let data = &[b'h', b'i', 0xE4, 0xB8]; assert_eq!(find_utf8_safe_end(data), 2); } + + #[test] + fn test_owned_resume_hint_suppresses_claude_footer_lines() { + assert!(should_suppress_resume_footer_line( + "Resume this session with:", + true, + )); + assert!(should_suppress_resume_footer_line( + " claude --resume abc-123", + true, + )); + assert!(!should_suppress_resume_footer_line( + "Resume this session with:", + false, + )); + } }