Skip to content

Commit deff661

Browse files
committed
Fix nested prompts taking over TUI
1 parent 815f597 commit deff661

4 files changed

Lines changed: 50 additions & 4 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "kit"
3-
version = "0.1.81"
3+
version = "0.1.82"
44
edition = "2024"
55
rust-version = "1.94.0"
66
publish = false
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Nested agent prompt could take over the TUI terminal
2+
3+
While using Kit as the agent harness, a subagent launched by another subagent ran a command that opened an interactive passphrase prompt through `/dev/tty`. Although ACP and tool processes use piped standard streams, they still inherited the TUI's controlling terminal. The prompt drew over Kit's interface and could leave the headless agent blocked waiting for user input.
4+
5+
## Resolution
6+
7+
The TUI now starts its headless `kit serve` backend in a new Unix session. The backend and all nested agents and tools therefore cannot open the TUI's controlling terminal. Interactive commands fail normally through their captured tool output instead of taking over the interface.

src/tui/mod.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,28 @@ const FAILURE_LINES: usize = 5;
8080
/// transcript it owns is closed out before the agent is killed.
8181
const SETTLE: Duration = Duration::from_secs(3);
8282

83+
#[cfg(unix)]
84+
fn detach_from_controlling_terminal(command: &mut tokio::process::Command) {
85+
use std::os::unix::process::CommandExt as _;
86+
87+
// The ACP backend is headless. A new session prevents it or any nested
88+
// agent/tool from opening the TUI's controlling terminal via /dev/tty.
89+
// SAFETY: `setsid` is async-signal-safe, and this closure only reports its
90+
// errno if it fails.
91+
unsafe {
92+
command.as_std_mut().pre_exec(|| {
93+
if libc::setsid() == -1 {
94+
Err(std::io::Error::last_os_error())
95+
} else {
96+
Ok(())
97+
}
98+
});
99+
}
100+
}
101+
102+
#[cfg(windows)]
103+
fn detach_from_controlling_terminal(_command: &mut tokio::process::Command) {}
104+
83105
fn current_model_choice(options: Option<&[SessionConfigOption]>) -> Option<ModelChoice> {
84106
let current = options
85107
.unwrap_or_default()
@@ -238,6 +260,7 @@ pub async fn run_with_reasoning_effort(
238260
if force {
239261
command.arg("--force");
240262
}
263+
detach_from_controlling_terminal(&mut command);
241264
let mut child = command
242265
.env(EVENTS_ENV, "1")
243266
.stdin(Stdio::piped())
@@ -1180,11 +1203,27 @@ mod tests {
11801203

11811204
use super::{
11821205
MAX_ATTACHMENTS, ModelChoice, attachments_from_paste, current_model_choice,
1183-
durable_session_id, effort_state, handle, message_of, osc52, prompt_blocks, readable,
1184-
refresh_config_state, save_effort_default_to, save_model_defaults_to, translate,
1206+
detach_from_controlling_terminal, durable_session_id, effort_state, handle, message_of,
1207+
osc52, prompt_blocks, readable, refresh_config_state, save_effort_default_to,
1208+
save_model_defaults_to, translate,
11851209
};
11861210
use crate::tui::app::{App, SubmittedPrompt, Update};
11871211

1212+
#[cfg(unix)]
1213+
#[tokio::test]
1214+
async fn agent_backend_starts_in_a_detached_session() {
1215+
let mut command = tokio::process::Command::new("sh");
1216+
command.arg("-c").arg("sleep 30");
1217+
detach_from_controlling_terminal(&mut command);
1218+
1219+
let mut child = command.spawn().unwrap();
1220+
let pid = i32::try_from(child.id().unwrap()).unwrap();
1221+
// SAFETY: `pid` belongs to the live child above.
1222+
assert_eq!(unsafe { libc::getsid(pid) }, pid);
1223+
child.kill().await.unwrap();
1224+
child.wait().await.unwrap();
1225+
}
1226+
11881227
#[test]
11891228
fn translates_available_commands_with_their_session() {
11901229
let updates = translate(SessionNotification::new(

0 commit comments

Comments
 (0)