aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJJ Liebig <jonathan.liebig@gmail.com>2026-09-05 02:38:14 +0200
committerGitHub <noreply@github.com>2026-09-05 02:38:14 +0200
commit7916be1656d8ee9c5882b52753cd47f5e58e3e7e (patch)
treed54015e01ecff6566bad26034ada4e8cca3d3edf /src
parent987651a760cdfe4cd9493388e8c16b8f99643a05 (diff)
fix: make Windows Codex prompt delay size-aware (#3552)
refs #3187
Diffstat (limited to 'src')
-rw-r--r--src/api/schema/agents.rs3
-rw-r--r--src/api/schema/tests.rs1
-rw-r--r--src/api/server.rs48
-rw-r--r--src/api/wait.rs64
-rw-r--r--src/app/api/agents.rs39
-rw-r--r--src/cli/agent.rs6
-rw-r--r--src/pane.rs16
-rw-r--r--src/pty/actor.rs141
-rw-r--r--src/terminal/runtime.rs4
9 files changed, 282 insertions, 40 deletions
diff --git a/src/api/schema/agents.rs b/src/api/schema/agents.rs
index 9e1e7b15..ee496d5d 100644
--- a/src/api/schema/agents.rs
+++ b/src/api/schema/agents.rs
@@ -37,6 +37,9 @@ pub struct AgentPromptWaitOptions {
pub until: Vec<AgentStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout_ms: Option<u64>,
+ #[serde(skip)]
+ #[schemars(skip)]
+ pub(crate) submission_deadline: Option<std::time::Instant>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
diff --git a/src/api/schema/tests.rs b/src/api/schema/tests.rs
index 06529f5e..693f0e85 100644
--- a/src/api/schema/tests.rs
+++ b/src/api/schema/tests.rs
@@ -133,6 +133,7 @@ fn agent_start_and_prompt_requests_round_trip() {
wait: Some(AgentPromptWaitOptions {
until: vec![AgentStatus::Idle, AgentStatus::Done],
timeout_ms: Some(120_000),
+ submission_deadline: None,
}),
}),
};
diff --git a/src/api/server.rs b/src/api/server.rs
index 64beec33..655d89c5 100644
--- a/src/api/server.rs
+++ b/src/api/server.rs
@@ -369,7 +369,7 @@ fn handle_request(
);
}
- dispatch_to_app(request, api_tx, None, response_write_complete, None)
+ dispatch_to_app(request, api_tx, None, response_write_complete, None, None)
}
pub(crate) fn api_method_name(method: &Method) -> &'static str {
@@ -798,7 +798,22 @@ pub(super) fn dispatch_to_app_with_timeout(
api_tx: &ApiRequestSender,
timeout: Option<Duration>,
) -> String {
- dispatch_to_app(request, api_tx, timeout, None, None)
+ dispatch_to_app(request, api_tx, timeout, None, None, None)
+}
+
+pub(super) fn dispatch_to_app_with_caller_timeout(
+ request: Request,
+ api_tx: &ApiRequestSender,
+ timeout: Option<Duration>,
+) -> String {
+ dispatch_to_app(
+ request,
+ api_tx,
+ timeout,
+ None,
+ None,
+ Some(("timeout", "timed out waiting for agent status")),
+ )
}
pub(super) fn dispatch_stream_open(
@@ -807,7 +822,7 @@ pub(super) fn dispatch_stream_open(
timeout: Duration,
active: Arc<AtomicBool>,
) -> String {
- dispatch_to_app(request, api_tx, Some(timeout), None, Some(active))
+ dispatch_to_app(request, api_tx, Some(timeout), None, Some(active), None)
}
pub(super) fn dispatch_stream_frame(
@@ -821,6 +836,7 @@ pub(super) fn dispatch_stream_frame(
Some(crate::app::pane_graphics::DIRECT_OUTER_TIMEOUT),
None,
Some(active),
+ None,
)
}
@@ -830,6 +846,7 @@ fn dispatch_to_app(
timeout: Option<Duration>,
response_write_complete: Option<std::sync::mpsc::Receiver<()>>,
stream_active: Option<Arc<AtomicBool>>,
+ timeout_response: Option<(&str, &str)>,
) -> String {
let request_id = request.id.clone();
let request_active = stream_active.clone();
@@ -875,6 +892,11 @@ fn dispatch_to_app(
if let Some(active) = request_active {
active.store(false, Ordering::Release);
}
+ if err.kind() == std::io::ErrorKind::TimedOut {
+ if let Some((code, message)) = timeout_response {
+ return error_response_json(request_id, code, message.into());
+ }
+ }
error_response_json(
request_id,
"server_unavailable",
@@ -884,6 +906,26 @@ fn dispatch_to_app(
}
}
+#[cfg(test)]
+#[test]
+fn caller_timeout_dispatch_uses_timeout_error() {
+ let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
+ let response = dispatch_to_app_with_caller_timeout(
+ Request {
+ id: "prompt-timeout".into(),
+ method: Method::AgentPrompt(crate::api::schema::AgentPromptParams {
+ target: "reviewer".into(),
+ text: "review this".into(),
+ wait: None,
+ }),
+ },
+ &tx,
+ Some(Duration::ZERO),
+ );
+ let error: ErrorResponse = serde_json::from_str(&response).unwrap();
+ assert_eq!(error.error.code, "timeout");
+}
+
fn error_response_json(id: String, code: &str, message: String) -> String {
serde_json::to_string(&ErrorResponse {
id,
diff --git a/src/api/wait.rs b/src/api/wait.rs
index 9403c4f2..541779f8 100644
--- a/src/api/wait.rs
+++ b/src/api/wait.rs
@@ -9,8 +9,8 @@ use crate::api::schema::{
SubscriptionEventEnvelope, SuccessResponse,
};
use crate::api::server::{
- dispatch_to_app_with_timeout, should_stop_connection, APP_RESPONSE_TIMEOUT,
- CONNECTION_POLL_INTERVAL,
+ dispatch_to_app_with_caller_timeout, dispatch_to_app_with_timeout, should_stop_connection,
+ APP_RESPONSE_TIMEOUT, CONNECTION_POLL_INTERVAL,
};
use crate::api::subscriptions::ActiveSubscription;
use crate::api::subscriptions::{match_output, output_match_read_source};
@@ -176,7 +176,7 @@ pub(super) fn wait_for_agent(
pub(super) fn prompt_agent(
request_id: String,
- params: crate::api::schema::AgentPromptParams,
+ mut params: crate::api::schema::AgentPromptParams,
stream: &mut LocalStream,
api_tx: &ApiRequestSender,
event_hub: &EventHub,
@@ -193,7 +193,14 @@ pub(super) fn prompt_agent(
)));
};
- let before_prompt = match agent_get(&request_id, &params.target, api_tx) {
+ let wait_started = std::time::Instant::now();
+ let before_prompt = match agent_get_for_prompt(
+ &request_id,
+ &params.target,
+ api_tx,
+ wait.timeout_ms,
+ wait_started,
+ ) {
Ok(agent) => agent,
Err(response) => {
return serde_json::to_string(&response)
@@ -204,16 +211,24 @@ pub(super) fn prompt_agent(
let prompt_started_working =
before_prompt.agent_status == crate::api::schema::AgentStatus::Working;
let target = params.target.clone();
- let wait_started = std::time::Instant::now();
+ if let Some(prompt_wait) = params.wait.as_mut() {
+ prompt_wait.submission_deadline = wait
+ .timeout_ms
+ .map(|timeout_ms| wait_started + std::time::Duration::from_millis(timeout_ms));
+ }
let last_event_sequence = event_hub.current_sequence();
- let prompt_response = dispatch_to_app_with_timeout(
- Request {
- id: request_id.clone(),
- method: Method::AgentPrompt(params),
- },
+ let prompt_request = Request {
+ id: request_id.clone(),
+ method: Method::AgentPrompt(params),
+ };
+ #[cfg(windows)]
+ let prompt_response = dispatch_to_app_with_caller_timeout(
+ prompt_request,
api_tx,
- None,
+ remaining_timeout_ms(wait.timeout_ms, wait_started).map(std::time::Duration::from_millis),
);
+ #[cfg(not(windows))]
+ let prompt_response = dispatch_to_app_with_timeout(prompt_request, api_tx, None);
let Ok(prompted) = agent_from_response(&request_id, &prompt_response) else {
return Ok(Some(prompt_response));
};
@@ -561,6 +576,33 @@ fn agent_get(
agent_from_response(request_id, &response)
}
+fn agent_get_for_prompt(
+ request_id: &str,
+ target: &str,
+ api_tx: &ApiRequestSender,
+ total_timeout_ms: Option<u64>,
+ started: std::time::Instant,
+) -> Result<crate::api::schema::AgentInfo, ErrorResponse> {
+ let request = Request {
+ id: format!("{request_id}:agent"),
+ method: Method::AgentGet(crate::api::schema::AgentTarget {
+ target: target.to_string(),
+ }),
+ };
+ let remaining_ms = remaining_timeout_ms(total_timeout_ms, started);
+ let response = match remaining_ms {
+ Some(timeout_ms) if timeout_ms <= APP_RESPONSE_TIMEOUT.as_millis() as u64 => {
+ dispatch_to_app_with_caller_timeout(
+ request,
+ api_tx,
+ Some(std::time::Duration::from_millis(timeout_ms)),
+ )
+ }
+ _ => dispatch_to_app_with_timeout(request, api_tx, Some(APP_RESPONSE_TIMEOUT)),
+ };
+ agent_from_response(request_id, &response)
+}
+
fn agent_from_response(
request_id: &str,
response: &str,
diff --git a/src/app/api/agents.rs b/src/app/api/agents.rs
index 3e4e83a8..cf3e5fa6 100644
--- a/src/app/api/agents.rs
+++ b/src/app/api/agents.rs
@@ -12,6 +12,18 @@ use super::responses::{encode_error, encode_error_body, encode_success};
const AGENT_PROMPT_SUBMIT_DELAY: Duration = Duration::from_millis(300);
+fn agent_prompt_submit_delay(agent: crate::detect::Agent, prompt_bytes: usize) -> Duration {
+ #[cfg(windows)]
+ if agent == crate::detect::Agent::Codex {
+ // Codex consumes Windows paste bursts at about 4 bytes/ms, then suppresses Enter briefly.
+ // ponytail: best-effort ConPTY timing; remove when Codex exposes a paste-complete boundary.
+ return Duration::from_millis(600 + prompt_bytes as u64 / 4);
+ }
+ #[cfg(not(windows))]
+ let _ = (agent, prompt_bytes);
+ AGENT_PROMPT_SUBMIT_DELAY
+}
+
impl App {
pub(super) fn handle_agent_list(&mut self, id: String) -> String {
encode_success(
@@ -72,6 +84,9 @@ impl App {
std::thread::spawn(move || {
let response = match completion.recv() {
Ok(Ok(())) => encode_success(id, ResponseResult::AgentPrompted { agent }),
+ Ok(Err(err)) if err.kind() == std::io::ErrorKind::TimedOut => {
+ encode_error(id, "timeout", err.to_string())
+ }
Ok(Err(err)) => encode_error(id, "agent_prompt_failed", err.to_string()),
Err(_) => encode_error(id, "agent_prompt_failed", "pty actor closed"),
};
@@ -149,6 +164,14 @@ impl App {
),
));
}
+ let submit_delay = agent_prompt_submit_delay(expected_agent, params.text.len());
+ #[cfg(windows)]
+ let submit_deadline = params
+ .wait
+ .as_ref()
+ .and_then(|wait| wait.submission_deadline);
+ #[cfg(not(windows))]
+ let submit_deadline = None;
if expected_agent == crate::detect::Agent::GithubCopilot {
// Copilot ignores synthetic Enter after focus loss until it receives focus gained.
let focus = match crate::ghostty::encode_focus(crate::ghostty::FocusEvent::Gained) {
@@ -170,7 +193,8 @@ impl App {
.queue_user_input_submission(
Bytes::from(text),
Bytes::from(enter),
- AGENT_PROMPT_SUBMIT_DELAY,
+ submit_delay,
+ submit_deadline,
)
.map_err(|err| encode_error(id.clone(), "agent_prompt_failed", err.to_string()))?;
Ok((id, agent, completion))
@@ -400,6 +424,19 @@ mod tests {
.expect("agent prompt responds after submission")
}
+ #[test]
+ fn prompt_delay_only_scales_for_windows_codex() {
+ let codex_delay = agent_prompt_submit_delay(Agent::Codex, 4_096);
+ #[cfg(windows)]
+ assert_eq!(codex_delay, Duration::from_millis(1_624));
+ #[cfg(not(windows))]
+ assert_eq!(codex_delay, AGENT_PROMPT_SUBMIT_DELAY);
+ assert_eq!(
+ agent_prompt_submit_delay(Agent::OpenCode, 4_096),
+ AGENT_PROMPT_SUBMIT_DELAY
+ );
+ }
+
#[tokio::test]
async fn agent_prompt_sends_text_then_delays_enter() {
let mut app = app_with_agent();
diff --git a/src/cli/agent.rs b/src/cli/agent.rs
index 42bc5e1c..1af3e0d6 100644
--- a/src/cli/agent.rs
+++ b/src/cli/agent.rs
@@ -834,7 +834,11 @@ fn agent_prompt(args: &[String]) -> std::io::Result<i32> {
method: Method::AgentPrompt(AgentPromptParams {
target: target.clone(),
text: text.clone(),
- wait: wait.then_some(AgentPromptWaitOptions { until, timeout_ms }),
+ wait: wait.then_some(AgentPromptWaitOptions {
+ until,
+ timeout_ms,
+ submission_deadline: None,
+ }),
}),
})?;
super::print_response(&response)
diff --git a/src/pane.rs b/src/pane.rs
index b7bd30bc..4f1ea0ad 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -1392,11 +1392,21 @@ impl PaneRuntimeIo {
text: Bytes,
enter: Bytes,
delay: std::time::Duration,
+ deadline: Option<std::time::Instant>,
) -> std::io::Result<std::sync::mpsc::Receiver<std::io::Result<()>>> {
match self {
- PaneRuntimeIo::Actor(actor) => actor.queue_user_input_submission(text, enter, delay),
+ PaneRuntimeIo::Actor(actor) => {
+ #[cfg(windows)]
+ return actor.queue_user_input_submission(text, enter, delay, deadline);
+ #[cfg(unix)]
+ {
+ let _ = deadline;
+ actor.queue_user_input_submission(text, enter, delay)
+ }
+ }
#[cfg(test)]
PaneRuntimeIo::TestChannel { sender, .. } => {
+ let _ = deadline;
let sender = sender.clone();
let (reply_tx, reply_rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
@@ -3112,8 +3122,10 @@ impl PaneRuntime {
text: Bytes,
enter: Bytes,
delay: std::time::Duration,
+ deadline: Option<std::time::Instant>,
) -> std::io::Result<std::sync::mpsc::Receiver<std::io::Result<()>>> {
- self.io.queue_user_input_submission(text, enter, delay)
+ self.io
+ .queue_user_input_submission(text, enter, delay, deadline)
}
pub fn try_send_paste(&self, text: String) -> Result<(), mpsc::error::TrySendError<Bytes>> {
diff --git a/src/pty/actor.rs b/src/pty/actor.rs
index 39b1d29f..76e793ef 100644
--- a/src/pty/actor.rs
+++ b/src/pty/actor.rs
@@ -8,7 +8,7 @@ pub(crate) use unix::*;
mod windows {
use std::io::{Read, Write};
use std::sync::{mpsc as std_mpsc, Arc, Mutex};
- use std::time::Duration;
+ use std::time::{Duration, Instant};
use bytes::Bytes;
use portable_pty::{MasterPty, PtySize};
@@ -49,6 +49,7 @@ mod windows {
text: Bytes,
enter: Bytes,
delay: Duration,
+ deadline: Option<Instant>,
reply: std_mpsc::Sender<std::io::Result<()>>,
},
}
@@ -57,6 +58,7 @@ mod windows {
Write(Bytes),
SubmissionPart {
bytes: Bytes,
+ deadline: Option<Instant>,
reply: std_mpsc::Sender<std::io::Result<()>>,
},
}
@@ -110,6 +112,7 @@ mod windows {
text: Bytes,
enter: Bytes,
delay: Duration,
+ deadline: Option<Instant>,
) -> std::io::Result<std_mpsc::Receiver<std::io::Result<()>>> {
let accepting = self
.accepting
@@ -127,6 +130,7 @@ mod windows {
text,
enter,
delay,
+ deadline,
reply: reply_tx,
})
.map_err(|err| match err {
@@ -292,9 +296,19 @@ mod windows {
for command in write_rx {
let result = match command {
PtyIoWriteCommand::Write(bytes) => write_and_flush(writer, &bytes),
- PtyIoWriteCommand::SubmissionPart { bytes, reply } => {
- let result = write_and_flush(writer, &bytes);
- let failed = result.is_err();
+ PtyIoWriteCommand::SubmissionPart {
+ bytes,
+ deadline,
+ reply,
+ } => {
+ let result = if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
+ Err(input_submission_timed_out())
+ } else {
+ write_and_flush(writer, &bytes)
+ };
+ let failed = result
+ .as_ref()
+ .is_err_and(|err| err.kind() != std::io::ErrorKind::TimedOut);
let _ = reply.send(result);
if failed {
break;
@@ -324,19 +338,32 @@ mod windows {
text,
enter,
delay,
+ deadline,
reply,
} => {
- let result = write_submission_part(&write_tx, text).and_then(|()| {
- std::thread::sleep(delay);
- let accepting = accepting
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner());
- if !*accepting {
- return Err(pty_actor_closed());
- }
- write_submission_part(&write_tx, enter)
- });
- let failed = result.is_err();
+ let result = if deadline.is_some_and(|deadline| {
+ deadline.saturating_duration_since(Instant::now()) <= delay
+ }) {
+ Err(input_submission_timed_out())
+ } else {
+ let text_deadline =
+ deadline.and_then(|deadline| deadline.checked_sub(delay));
+ write_submission_part(&write_tx, text, text_deadline).and_then(|()| {
+ // A started text write is committed. Finish Enter even if the caller
+ // stops waiting so a timeout cannot leave a partial prompt.
+ std::thread::sleep(delay);
+ let accepting = accepting
+ .lock()
+ .unwrap_or_else(|poisoned| poisoned.into_inner());
+ if !*accepting {
+ return Err(pty_actor_closed());
+ }
+ write_submission_part(&write_tx, enter, None)
+ })
+ };
+ let failed = result
+ .as_ref()
+ .is_err_and(|err| err.kind() != std::io::ErrorKind::TimedOut);
let _ = reply.send(result);
if failed {
break;
@@ -349,10 +376,15 @@ mod windows {
fn write_submission_part(
write_tx: &std_mpsc::Sender<PtyIoWriteCommand>,
bytes: Bytes,
+ deadline: Option<Instant>,
) -> std::io::Result<()> {
let (reply, completion) = std_mpsc::channel();
write_tx
- .send(PtyIoWriteCommand::SubmissionPart { bytes, reply })
+ .send(PtyIoWriteCommand::SubmissionPart {
+ bytes,
+ deadline,
+ reply,
+ })
.map_err(|_| pty_actor_closed())?;
completion
.recv()
@@ -363,6 +395,13 @@ mod windows {
std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pty actor closed")
}
+ fn input_submission_timed_out() -> std::io::Error {
+ std::io::Error::new(
+ std::io::ErrorKind::TimedOut,
+ "agent prompt timed out before input submission",
+ )
+ }
+
fn write_and_flush(writer: &mut impl Write, bytes: &[u8]) -> std::io::Result<()> {
writer.write_all(bytes)?;
writer.flush()
@@ -371,8 +410,6 @@ mod windows {
#[cfg(test)]
mod tests {
use super::*;
- use std::time::Instant;
-
struct RecordingWriter {
writes: Vec<(Vec<u8>, Instant)>,
flushes: Vec<Instant>,
@@ -402,6 +439,7 @@ mod windows {
fn run_recorded_submission(
fail_after: Option<usize>,
delay: Duration,
+ deadline: Option<Instant>,
during_delay: impl FnOnce(&std_mpsc::Sender<PtyIoWriteCommand>, &Arc<Mutex<bool>>),
) -> (RecordingWriter, std::io::Result<()>) {
let (flushed_tx, flushed_rx) = std_mpsc::channel();
@@ -420,6 +458,7 @@ mod windows {
text: Bytes::from_static(b"prompt"),
enter: Bytes::from_static(b"\r"),
delay,
+ deadline,
reply: reply_tx,
})
.unwrap();
@@ -449,7 +488,7 @@ mod windows {
#[test]
fn submission_sequences_user_input_but_allows_terminal_responses() {
let delay = Duration::from_millis(30);
- let (writer, result) = run_recorded_submission(None, delay, |write_tx, _| {
+ let (writer, result) = run_recorded_submission(None, delay, None, |write_tx, _| {
write_tx
.send(PtyIoWriteCommand::Write(Bytes::from_static(b"response")))
.unwrap();
@@ -465,7 +504,8 @@ mod windows {
#[test]
fn submission_returns_enter_write_failure() {
- let (_writer, result) = run_recorded_submission(Some(1), Duration::ZERO, |_, _| {});
+ let (_writer, result) =
+ run_recorded_submission(Some(1), Duration::ZERO, None, |_, _| {});
let err = result.expect_err("enter failure reaches caller");
assert_eq!(err.kind(), std::io::ErrorKind::BrokenPipe);
@@ -474,7 +514,7 @@ mod windows {
#[test]
fn shutdown_during_submission_delay_cancels_enter() {
let (writer, result) =
- run_recorded_submission(None, Duration::from_millis(30), |_, accepting| {
+ run_recorded_submission(None, Duration::from_millis(30), None, |_, accepting| {
*accepting.lock().unwrap() = false;
});
let err = result.expect_err("shutdown cancels enter");
@@ -488,6 +528,65 @@ mod windows {
vec![b"prompt"]
);
}
+
+ #[test]
+ fn expired_queued_submission_is_not_written() {
+ let (flushed_tx, _flushed_rx) = std_mpsc::channel();
+ let mut writer = RecordingWriter {
+ writes: Vec::new(),
+ flushes: Vec::new(),
+ fail_after: None,
+ flushed: flushed_tx,
+ };
+ let (data_tx, mut data_rx) = mpsc::channel(2);
+ let (write_tx, write_rx) = std_mpsc::channel();
+ let (first_reply_tx, first_reply_rx) = std_mpsc::channel();
+ let (expired_reply_tx, expired_reply_rx) = std_mpsc::channel();
+ let accepting = Arc::new(Mutex::new(true));
+ data_tx
+ .try_send(PtyIoDataCommand::SubmitUserInput {
+ text: Bytes::from_static(b"first"),
+ enter: Bytes::from_static(b"\r"),
+ delay: Duration::from_millis(30),
+ deadline: None,
+ reply: first_reply_tx,
+ })
+ .unwrap();
+ data_tx
+ .try_send(PtyIoDataCommand::SubmitUserInput {
+ text: Bytes::from_static(b"expired"),
+ enter: Bytes::from_static(b"\r"),
+ delay: Duration::ZERO,
+ deadline: Some(Instant::now() + Duration::from_millis(10)),
+ reply: expired_reply_tx,
+ })
+ .unwrap();
+
+ let writer_thread = std::thread::spawn(move || {
+ run_writer(&mut writer, write_rx);
+ writer
+ });
+ let input_write_tx = write_tx.clone();
+ let input_thread = std::thread::spawn(move || {
+ run_input_forwarder(&mut data_rx, input_write_tx, accepting)
+ });
+ first_reply_rx.recv().unwrap().unwrap();
+ let err = expired_reply_rx.recv().unwrap().unwrap_err();
+ assert_eq!(err.kind(), std::io::ErrorKind::TimedOut);
+
+ drop(data_tx);
+ input_thread.join().unwrap();
+ drop(write_tx);
+ let writer = writer_thread.join().unwrap();
+ assert_eq!(
+ writer
+ .writes
+ .iter()
+ .map(|write| write.0.as_slice())
+ .collect::<Vec<_>>(),
+ vec![b"first".as_slice(), b"\r".as_slice()]
+ );
+ }
}
}
diff --git a/src/terminal/runtime.rs b/src/terminal/runtime.rs
index 66484ea9..4fa80326 100644
--- a/src/terminal/runtime.rs
+++ b/src/terminal/runtime.rs
@@ -454,8 +454,10 @@ impl TerminalRuntime {
text: Bytes,
enter: Bytes,
delay: std::time::Duration,
+ deadline: Option<std::time::Instant>,
) -> std::io::Result<std::sync::mpsc::Receiver<std::io::Result<()>>> {
- self.0.queue_user_input_submission(text, enter, delay)
+ self.0
+ .queue_user_input_submission(text, enter, delay, deadline)
}
pub fn try_send_paste(&self, text: String) -> Result<(), mpsc::error::TrySendError<Bytes>> {