aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJJ Liebig <jonathan.liebig@gmail.com>2026-09-11 17:04:06 +0200
committerGitHub <noreply@github.com>2026-09-11 17:04:06 +0200
commit871ff8b91f969a0d6d7fcd21ad028723a77bac6c (patch)
tree16b5c0fee1b650a3432e7bd6e48bd1f1d2ca5cb2 /src
parentf3cbe03fdb07b860a4543c684fb81c5b346e6913 (diff)
fix: flush Windows Codex paste burst before prompt submit (#3961)
Codex's Windows input reader does not surface bracketed paste. It buffers the prompt as a paste burst and rewrites a following Enter into a newline until the burst idles out, so the previous size-based delay only reduced the chance of losing the submit. Append a non-character key after the paste so Codex flushes the burst synchronously, giving every prompt a deterministic submit boundary. refs #3187
Diffstat (limited to 'src')
-rw-r--r--src/app/api/agents.rs77
1 files changed, 56 insertions, 21 deletions
diff --git a/src/app/api/agents.rs b/src/app/api/agents.rs
index cf3e5fa6..1c0a5369 100644
--- a/src/app/api/agents.rs
+++ b/src/app/api/agents.rs
@@ -12,16 +12,24 @@ 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);
+// Codex's Windows input reader does not surface bracketed paste. It detects the prompt as a
+// "paste burst" and, while that burst is buffered, rewrites a following Enter into a newline
+// instead of submitting. The burst only flushes after an idle timeout, so any size-based delay is
+// a timing guess that fails when ConPTY delivery lags it. Codex flushes a buffered burst
+// synchronously when it receives a non-character key, so appending one after the paste gives the
+// submission a deterministic paste boundary regardless of prompt size or delivery speed.
+#[cfg(windows)]
+fn append_codex_paste_boundary(runtime: &crate::terminal::TerminalRuntime, text: &mut Vec<u8>) {
+ let keys = match crate::app::api_helpers::encode_api_keys(runtime, &["right".to_string()]) {
+ Ok(keys) => keys,
+ Err(key) => {
+ tracing::warn!(key = %key, "failed to encode Codex paste boundary key");
+ return;
+ }
+ };
+ if let Some(key) = keys.into_iter().find(|bytes| !bytes.is_empty()) {
+ text.extend_from_slice(&key);
}
- #[cfg(not(windows))]
- let _ = (agent, prompt_bytes);
- AGENT_PROMPT_SUBMIT_DELAY
}
impl App {
@@ -164,7 +172,6 @@ impl App {
),
));
}
- let submit_delay = agent_prompt_submit_delay(expected_agent, params.text.len());
#[cfg(windows)]
let submit_deadline = params
.wait
@@ -186,6 +193,14 @@ impl App {
}
let (text, enter) =
crate::app::api_helpers::encode_api_submission_parts(runtime, &params.text);
+ #[cfg(windows)]
+ let text = if expected_agent == crate::detect::Agent::Codex {
+ let mut text = text;
+ append_codex_paste_boundary(runtime, &mut text);
+ text
+ } else {
+ text
+ };
let Some(agent) = self.agent_info(resolved.ws_idx, resolved.pane_id) else {
return Err(agent_not_found(id, &params.target));
};
@@ -193,7 +208,7 @@ impl App {
.queue_user_input_submission(
Bytes::from(text),
Bytes::from(enter),
- submit_delay,
+ AGENT_PROMPT_SUBMIT_DELAY,
submit_deadline,
)
.map_err(|err| encode_error(id.clone(), "agent_prompt_failed", err.to_string()))?;
@@ -424,17 +439,37 @@ 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
+ #[cfg(windows)]
+ #[tokio::test]
+ async fn windows_codex_prompt_flushes_paste_burst_before_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]
+ .attached_terminal_id
+ .clone();
+ let terminal = app.state.terminals.get_mut(&terminal_id).unwrap();
+ terminal.set_agent_name("reviewer".into());
+ terminal.set_detected_state(Some(Agent::Codex), AgentState::Idle);
+ let (runtime, mut rx) = crate::terminal::TerminalRuntime::test_with_channel(80, 24);
+ app.state.insert_test_runtime(pane_id, runtime);
+
+ let response = run_deferred_agent_prompt(
+ &mut app,
+ "req",
+ AgentPromptParams {
+ target: "reviewer".into(),
+ text: "A != B".into(),
+ wait: None,
+ },
);
+ let success: SuccessResponse = serde_json::from_str(&response).unwrap();
+ assert!(matches!(
+ success.result,
+ ResponseResult::AgentPrompted { .. }
+ ));
+ // The non-character key must precede Enter so Codex commits the paste burst first.
+ assert_eq!(rx.try_recv().unwrap(), Bytes::from_static(b"A != B\x1b[C"));
+ assert_eq!(rx.try_recv().unwrap(), Bytes::from_static(b"\r"));
}
#[tokio::test]