aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-27 00:12:25 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-27 00:12:25 +0300
commitbb29eedb7209a0d5e91052458ce76bc7e4259d18 (patch)
tree6c2e3f85dbf2666e37bd3c977d587e0ed5f15d25 /src
parent471041690af928d9a4d4cda8e4963cc7dcb3d6a8 (diff)
fix: delay agent prompt submission
refs #1878
Diffstat (limited to 'src')
-rw-r--r--src/app/api/agents.rs34
-rw-r--r--src/app/api_helpers.rs18
-rw-r--r--src/pane.rs26
-rw-r--r--src/terminal/runtime.rs4
4 files changed, 72 insertions, 10 deletions
diff --git a/src/app/api/agents.rs b/src/app/api/agents.rs
index f1977ce4..8bc29462 100644
--- a/src/app/api/agents.rs
+++ b/src/app/api/agents.rs
@@ -1,3 +1,5 @@
+use std::time::Duration;
+
use bytes::Bytes;
use crate::api::schema::{
@@ -8,6 +10,8 @@ use crate::app::App;
use super::responses::{encode_error, encode_error_body, encode_success};
+const AGENT_PROMPT_SUBMIT_DELAY: Duration = Duration::from_millis(300);
+
impl App {
pub(super) fn handle_agent_list(&mut self, id: String) -> String {
encode_success(
@@ -94,10 +98,12 @@ impl App {
),
);
}
- let bytes = crate::app::api_helpers::encode_api_submission(runtime, &params.text);
- if let Err(err) = runtime.try_send_bytes(Bytes::from(bytes)) {
+ let (text, enter) =
+ crate::app::api_helpers::encode_api_submission_parts(runtime, &params.text);
+ if let Err(err) = runtime.try_send_bytes(Bytes::from(text)) {
return encode_error(id, "agent_prompt_failed", err.to_string());
}
+ runtime.send_bytes_after(Bytes::from(enter), AGENT_PROMPT_SUBMIT_DELAY);
let Some(agent) = self.agent_info(resolved.ws_idx, resolved.pane_id) else {
return agent_not_found(id, &params.target);
};
@@ -307,7 +313,7 @@ mod tests {
}
#[tokio::test]
- async fn agent_prompt_accepts_pane_ids_and_working_agents_atomically() {
+ async fn agent_prompt_sends_text_then_delays_enter() {
let mut app = app_with_agent();
let pane_id = app.state.workspaces[0].tabs[0].root_pane;
let terminal_id = app.state.workspaces[0].tabs[0].panes[&pane_id]
@@ -324,6 +330,7 @@ mod tests {
app.state.insert_test_runtime(pane_id, runtime);
let public_pane_id = app.public_pane_id(0, pane_id).unwrap();
+ let bracketed_started = std::time::Instant::now();
let response = app.handle_agent_prompt(
"req".into(),
AgentPromptParams {
@@ -339,13 +346,22 @@ mod tests {
assert_eq!(agent.name.as_deref(), Some("reviewer"));
assert_eq!(
rx.try_recv().unwrap(),
- Bytes::from_static(b"\x1b[200~A != B\x1b[201~\r")
+ Bytes::from_static(b"\x1b[200~A != B\x1b[201~")
);
assert!(rx.try_recv().is_err());
+ assert_eq!(
+ tokio::time::timeout(Duration::from_secs(1), rx.recv())
+ .await
+ .unwrap()
+ .unwrap(),
+ Bytes::from_static(b"\r")
+ );
+ assert!(bracketed_started.elapsed() >= AGENT_PROMPT_SUBMIT_DELAY);
app.lookup_runtime_sender(0, pane_id)
.unwrap()
.test_process_pty_bytes(b"\x1b[?2004l");
+ let raw_started = std::time::Instant::now();
let raw = app.handle_agent_prompt(
"req-raw".into(),
AgentPromptParams {
@@ -356,8 +372,16 @@ mod tests {
);
let raw: SuccessResponse = serde_json::from_str(&raw).unwrap();
assert!(matches!(raw.result, ResponseResult::AgentPrompted { .. }));
- assert_eq!(rx.try_recv().unwrap(), Bytes::from_static(b"A != B\r"));
+ assert_eq!(rx.try_recv().unwrap(), Bytes::from_static(b"A != B"));
assert!(rx.try_recv().is_err());
+ assert_eq!(
+ tokio::time::timeout(Duration::from_secs(1), rx.recv())
+ .await
+ .unwrap()
+ .unwrap(),
+ Bytes::from_static(b"\r")
+ );
+ assert!(raw_started.elapsed() >= AGENT_PROMPT_SUBMIT_DELAY);
let rejected = app.handle_agent_prompt(
"req-label".into(),
diff --git a/src/app/api_helpers.rs b/src/app/api_helpers.rs
index e0091b39..a6dad384 100644
--- a/src/app/api_helpers.rs
+++ b/src/app/api_helpers.rs
@@ -48,17 +48,25 @@ pub(super) fn encode_api_keys(
Ok(encoded_keys)
}
-pub(super) fn encode_api_submission(
+pub(super) fn encode_api_submission_parts(
runtime: &crate::terminal::TerminalRuntime,
text: &str,
-) -> Vec<u8> {
- let mut bytes = encode_api_text(runtime, text);
+) -> (Vec<u8>, Vec<u8>) {
+ let text = encode_api_text(runtime, text);
let enter = crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Enter,
crossterm::event::KeyModifiers::NONE,
);
- bytes.extend_from_slice(&runtime.encode_terminal_key(enter.into()));
- bytes
+ (text, runtime.encode_terminal_key(enter.into()))
+}
+
+pub(super) fn encode_api_submission(
+ runtime: &crate::terminal::TerminalRuntime,
+ text: &str,
+) -> Vec<u8> {
+ let (mut text, enter) = encode_api_submission_parts(runtime, text);
+ text.extend_from_slice(&enter);
+ text
}
pub(super) fn encode_api_input(
diff --git a/src/pane.rs b/src/pane.rs
index 45f4f720..c6d8c003 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -1107,6 +1107,28 @@ impl PaneRuntimeIo {
PaneRuntimeIo::TestChannel { sender, .. } => sender.try_send(bytes),
}
}
+
+ fn send_bytes_after(&self, bytes: Bytes, delay: std::time::Duration) {
+ match self {
+ PaneRuntimeIo::Actor(actor) => {
+ let actor = actor.clone();
+ tokio::spawn(async move {
+ tokio::time::sleep(delay).await;
+ if let Err(err) = actor.write_user_input(bytes).await {
+ warn!(error = %err, "failed to send delayed PTY input");
+ }
+ });
+ }
+ #[cfg(test)]
+ PaneRuntimeIo::TestChannel { sender, .. } => {
+ let sender = sender.clone();
+ tokio::spawn(async move {
+ tokio::time::sleep(delay).await;
+ let _ = sender.send(bytes).await;
+ });
+ }
+ }
+ }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -2628,6 +2650,10 @@ impl PaneRuntime {
self.io.try_send_bytes(bytes)
}
+ pub fn send_bytes_after(&self, bytes: Bytes, delay: std::time::Duration) {
+ self.io.send_bytes_after(bytes, delay);
+ }
+
pub async fn send_paste(&self, text: String) -> Result<(), mpsc::error::SendError<Bytes>> {
self.send_bytes(self.paste_payload(text)).await
}
diff --git a/src/terminal/runtime.rs b/src/terminal/runtime.rs
index cd6f4211..f9858ce0 100644
--- a/src/terminal/runtime.rs
+++ b/src/terminal/runtime.rs
@@ -411,6 +411,10 @@ impl TerminalRuntime {
self.0.try_send_bytes(bytes)
}
+ pub fn send_bytes_after(&self, bytes: Bytes, delay: std::time::Duration) {
+ self.0.send_bytes_after(bytes, delay);
+ }
+
pub async fn send_paste(&self, text: String) -> Result<(), mpsc::error::SendError<Bytes>> {
self.0.send_paste(text).await
}