diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-07 15:09:09 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-07 15:09:09 +0300 |
| commit | 0276da6f2a44da493d1c198bcc6b5a8b1d2fda44 (patch) | |
| tree | 4362a0c52fa42097d2c6fabc8212e4d01ddd459c | |
| parent | 78fc0721a3d014fe96e778c6bb452cfdfbe67603 (diff) | |
fix: make pane run submit commands atomically
| -rw-r--r-- | SKILL.md | 2 | ||||
| -rw-r--r-- | SOCKET_API.md | 4 | ||||
| -rw-r--r-- | src/cli.rs | 15 | ||||
| -rw-r--r-- | tests/cli_wrapper.rs | 68 |
4 files changed, 72 insertions, 17 deletions
@@ -165,7 +165,7 @@ press Enter or other keys: herdr pane send-keys 1-1 Enter ``` -`pane run` combines both — sends text, then presses Enter: +`pane run` appends a trailing carriage return (`\r`) to the text so the command executes: ```bash herdr pane run 1-1 "echo hello" diff --git a/SOCKET_API.md b/SOCKET_API.md index c145aa53..ad5a80e7 100644 --- a/SOCKET_API.md +++ b/SOCKET_API.md @@ -830,7 +830,7 @@ herdr wait agent-state <pane_id> --state <idle|working|blocked|unknown> [--timeo - `pane read --source recent-unwrapped` returns recent terminal text with soft wraps joined back together - `pane send-text`, `pane send-keys`, and `pane run` print nothing on success - list/get/create/split/wait commands print json on success -- `pane run` is a convenience wrapper for `pane send-text` + `pane send-keys Enter` +- `pane run` is a convenience wrapper for `pane send-text` with a trailing carriage return (`\r`) - `wait agent-state` is a cli convenience built on top of event subscriptions - `--raw` disables ansi stripping for `pane read` and `wait output` - `wait output --source recent` matches against unwrapped recent terminal text by default, so pane width and soft wrapping do not break matches @@ -860,7 +860,7 @@ herdr pane read 1-1 --source recent --lines 80 ## behavior notes and gotchas -- `pane.send_text` sends literal text only. if you want to execute a command, follow it with `pane.send_keys` and `Enter`, or use cli `pane run`. +- `pane.send_text` sends literal text only. if you want to execute a command, follow it with `pane.send_keys` and `Enter`, or use cli `pane run`, which appends a trailing carriage return (`\r`). - `pane.read` and `pane.wait_for_output` strip ansi by default. - `pane.output_matched` subscriptions fire on transitions into a matching state; they do not repeatedly spam the same still-visible match on every poll. - closing the socket connection ends the subscription. @@ -596,19 +596,8 @@ fn pane_run(args: &[String]) -> std::io::Result<i32> { } let pane_id = normalize_pane_id(&args[0]); - let text = args[1..].join(" "); - let send_text_status = send_ok_request(Method::PaneSendText(PaneSendTextParams { - pane_id: pane_id.clone(), - text, - }))?; - if send_text_status != 0 { - return Ok(send_text_status); - } - - send_ok_request(Method::PaneSendKeys(PaneSendKeysParams { - pane_id, - keys: vec!["Enter".into()], - })) + let text = format!("{}\r", args[1..].join(" ")); + send_ok_request(Method::PaneSendText(PaneSendTextParams { pane_id, text })) } fn integration_install(args: &[String]) -> std::io::Result<i32> { diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs index dc091e80..97d309a4 100644 --- a/tests/cli_wrapper.rs +++ b/tests/cli_wrapper.rs @@ -2,7 +2,7 @@ use std::fs; use std::io::{BufRead, BufReader, Write}; -use std::os::unix::net::UnixStream; +use std::os::unix::net::{UnixListener, UnixStream}; use std::path::{Path, PathBuf}; use std::process::Command; use std::thread; @@ -126,6 +126,72 @@ fn send_request(socket_path: &Path, json: &str) -> serde_json::Value { } #[test] +fn pane_run_sends_one_send_text_request_with_trailing_carriage_return() { + let base = unique_test_dir(); + fs::create_dir_all(&base).unwrap(); + let socket_path = base.join("herdr.sock"); + let listener = UnixListener::bind(&socket_path).unwrap(); + + let server = thread::spawn(move || { + let (mut first_stream, _) = listener.accept().unwrap(); + let mut first_line = String::new(); + let mut first_reader = BufReader::new(first_stream.try_clone().unwrap()); + first_reader.read_line(&mut first_line).unwrap(); + first_stream + .write_all(br#"{"id":"cli:request","result":{"type":"ok"}}"#) + .unwrap(); + first_stream.write_all(b"\n").unwrap(); + first_stream.flush().unwrap(); + + let mut second_line = None; + listener.set_nonblocking(true).unwrap(); + let deadline = Instant::now() + Duration::from_millis(250); + while Instant::now() < deadline { + match listener.accept() { + Ok((mut second_stream, _)) => { + let mut line = String::new(); + let mut reader = BufReader::new(second_stream.try_clone().unwrap()); + reader.read_line(&mut line).unwrap(); + second_stream + .write_all(br#"{"id":"cli:request","result":{"type":"ok"}}"#) + .unwrap(); + second_stream.write_all(b"\n").unwrap(); + second_stream.flush().unwrap(); + second_line = Some(line); + break; + } + Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { + thread::sleep(Duration::from_millis(10)); + } + Err(err) => panic!("second accept failed: {err}"), + } + } + + (first_line, second_line) + }); + + let run = run_cli(&socket_path, &["pane", "run", "1-1", "echo hello"]); + assert!( + run.status.success(), + "stderr: {}", + String::from_utf8_lossy(&run.stderr) + ); + + let (first_line, second_line) = server.join().unwrap(); + let first_request: serde_json::Value = serde_json::from_str(&first_line).unwrap(); + assert_eq!(first_request["method"], "pane.send_text"); + assert_eq!(first_request["params"]["pane_id"], "1-1"); + assert_eq!(first_request["params"]["text"], "echo hello\r"); + assert!( + second_line.is_none(), + "pane run sent an unexpected second request: {:?}", + second_line + ); + + fs::remove_dir_all(base).unwrap(); +} + +#[test] fn workspace_and_pane_management_commands_work() { let base = unique_test_dir(); let config_home = base.join("config"); |
