parent
471041690a
commit
bb29eedb72
|
|
@ -6,6 +6,7 @@
|
|||
- Relicensed Herdr from AGPL-3.0-or-later to Apache-2.0.
|
||||
|
||||
### Fixed
|
||||
- Agent prompts now wait briefly after sending text before pressing Enter, preventing prompts from remaining in agent composers without starting a turn. (#1878)
|
||||
- Empty clipboard writes from pane applications no longer erase existing clipboard contents or show a copied confirmation. (#1893)
|
||||
- Plain mouse movement no longer triggers continuous full renders while preserving Herdr menu hover and pane application mouse tracking. (#1865)
|
||||
- `ui.copy_on_select = false` now retains drag and double-click word selections without copying; `Ctrl+C`, or `Cmd+C` when the host terminal forwards it, copies and clears the selection.
|
||||
|
|
|
|||
|
|
@ -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, ¶ms.text);
|
||||
if let Err(err) = runtime.try_send_bytes(Bytes::from(bytes)) {
|
||||
let (text, enter) =
|
||||
crate::app::api_helpers::encode_api_submission_parts(runtime, ¶ms.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, ¶ms.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(),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
26
src/pane.rs
26
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ fn agent_start_command_works() {
|
|||
"do not transition",
|
||||
"--wait",
|
||||
"--timeout",
|
||||
"200",
|
||||
"500",
|
||||
],
|
||||
);
|
||||
assert_eq!(stale_idle.status.code(), Some(1));
|
||||
|
|
|
|||
Loading…
Reference in New Issue