feat: implement ACP user-interaction to request_permission bridge
This commit is contained in:
+41
-1
@@ -28,7 +28,8 @@ where
|
|||||||
R: tokio::io::AsyncRead + Unpin,
|
R: tokio::io::AsyncRead + Unpin,
|
||||||
W: AsyncWrite + Unpin,
|
W: AsyncWrite + Unpin,
|
||||||
{
|
{
|
||||||
use crate::utils::create_abort_signal;
|
use crate::utils::{create_abort_signal, drain_acp_permissions};
|
||||||
|
drain_acp_permissions();
|
||||||
let state = AcpServerState {
|
let state = AcpServerState {
|
||||||
ctx: None,
|
ctx: None,
|
||||||
abort: create_abort_signal(),
|
abort: create_abort_signal(),
|
||||||
@@ -55,6 +56,9 @@ where
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(response) = dispatch(&line, &mut state).await {
|
if let Some(response) = dispatch(&line, &mut state).await {
|
||||||
|
for params in crate::utils::drain_acp_permissions() {
|
||||||
|
emit_notification(&mut writer, "session/request_permission", params).await?;
|
||||||
|
}
|
||||||
emit(&mut writer, &response).await?;
|
emit(&mut writer, &response).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,6 +213,23 @@ async fn emit<W: AsyncWrite + Unpin>(writer: &mut W, response: &Response) -> Res
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn emit_notification<W: AsyncWrite + Unpin>(
|
||||||
|
writer: &mut W,
|
||||||
|
method: &str,
|
||||||
|
params: serde_json::Value,
|
||||||
|
) -> Result<()> {
|
||||||
|
let frame = serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": method,
|
||||||
|
"params": params,
|
||||||
|
});
|
||||||
|
let mut line = serde_json::to_string(&frame)?;
|
||||||
|
line.push('\n');
|
||||||
|
writer.write_all(line.as_bytes()).await?;
|
||||||
|
writer.flush().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -453,4 +474,23 @@ mod tests {
|
|||||||
assert_eq!(v["id"], 6);
|
assert_eq!(v["id"], 6);
|
||||||
assert!(v["error"].is_object());
|
assert!(v["error"].is_object());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn emit_notification_produces_valid_json_rpc_frame() {
|
||||||
|
let mut output = Vec::new();
|
||||||
|
emit_notification(
|
||||||
|
&mut output,
|
||||||
|
"session/request_permission",
|
||||||
|
serde_json::json!({"action": "confirm", "question": "Proceed?"}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let s = String::from_utf8(output).unwrap();
|
||||||
|
let v: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
|
||||||
|
assert_eq!(v["jsonrpc"], "2.0");
|
||||||
|
assert_eq!(v["method"], "session/request_permission");
|
||||||
|
assert!(v["params"]["action"].is_string());
|
||||||
|
assert!(!v.as_object().unwrap().contains_key("id"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::{FunctionDeclaration, JsonSchema};
|
use super::{FunctionDeclaration, JsonSchema};
|
||||||
use crate::config::RequestContext;
|
use crate::config::RequestContext;
|
||||||
use crate::supervisor::escalation::{EscalationRequest, new_escalation_id};
|
use crate::supervisor::escalation::{EscalationRequest, new_escalation_id};
|
||||||
use crate::utils::HEADLESS;
|
use crate::utils::{ACP_SERVER, HEADLESS, queue_acp_permission};
|
||||||
|
|
||||||
use anyhow::{Result, anyhow, bail};
|
use anyhow::{Result, anyhow, bail};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
@@ -138,6 +138,16 @@ pub async fn handle_user_tool(
|
|||||||
.strip_prefix(USER_FUNCTION_PREFIX)
|
.strip_prefix(USER_FUNCTION_PREFIX)
|
||||||
.unwrap_or(cmd_name);
|
.unwrap_or(cmd_name);
|
||||||
|
|
||||||
|
if ACP_SERVER.load(Ordering::SeqCst) {
|
||||||
|
let result = handle_headless(action, args);
|
||||||
|
queue_acp_permission(json!({
|
||||||
|
"action": action,
|
||||||
|
"question": result["question"],
|
||||||
|
"options": result["options"],
|
||||||
|
}));
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
if HEADLESS.load(Ordering::SeqCst) {
|
if HEADLESS.load(Ordering::SeqCst) {
|
||||||
return Ok(handle_headless(action, args));
|
return Ok(handle_headless(action, args));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ async fn main() -> Result<()> {
|
|||||||
env::set_var("AUTO_CONFIRM", "true");
|
env::set_var("AUTO_CONFIRM", "true");
|
||||||
}
|
}
|
||||||
HEADLESS.store(true, Ordering::SeqCst);
|
HEADLESS.store(true, Ordering::SeqCst);
|
||||||
|
if cli.acp_server {
|
||||||
|
ACP_SERVER.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let info_flag = cli.info
|
let info_flag = cli.info
|
||||||
|
|||||||
+18
-1
@@ -33,8 +33,9 @@ use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
|
|||||||
use is_terminal::IsTerminal;
|
use is_terminal::IsTerminal;
|
||||||
use nu_ansi_term::Color;
|
use nu_ansi_term::Color;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::{LazyLock, OnceLock};
|
use std::sync::{LazyLock, Mutex, OnceLock};
|
||||||
use std::{cmp, env, path::PathBuf, process};
|
use std::{cmp, env, path::PathBuf, process};
|
||||||
use syntect::highlighting::{Highlighter, Theme};
|
use syntect::highlighting::{Highlighter, Theme};
|
||||||
use syntect::parsing::Scope;
|
use syntect::parsing::Scope;
|
||||||
@@ -45,6 +46,22 @@ pub static THINK_TAG_RE: LazyLock<Regex> =
|
|||||||
LazyLock::new(|| Regex::new(r"(?s)^\s*<think>.*?</think>(\s*|$)").unwrap());
|
LazyLock::new(|| Regex::new(r"(?s)^\s*<think>.*?</think>(\s*|$)").unwrap());
|
||||||
pub static IS_STDOUT_TERMINAL: LazyLock<bool> = LazyLock::new(|| std::io::stdout().is_terminal());
|
pub static IS_STDOUT_TERMINAL: LazyLock<bool> = LazyLock::new(|| std::io::stdout().is_terminal());
|
||||||
pub static HEADLESS: AtomicBool = AtomicBool::new(false);
|
pub static HEADLESS: AtomicBool = AtomicBool::new(false);
|
||||||
|
pub static ACP_SERVER: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
static ACP_PERMISSION_QUEUE: Mutex<VecDeque<serde_json::Value>> = Mutex::new(VecDeque::new());
|
||||||
|
|
||||||
|
pub fn queue_acp_permission(notification: serde_json::Value) {
|
||||||
|
if let Ok(mut q) = ACP_PERMISSION_QUEUE.lock() {
|
||||||
|
q.push_back(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn drain_acp_permissions() -> Vec<serde_json::Value> {
|
||||||
|
ACP_PERMISSION_QUEUE
|
||||||
|
.lock()
|
||||||
|
.map(|mut q| q.drain(..).collect())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
pub static NO_COLOR: LazyLock<bool> = LazyLock::new(|| {
|
pub static NO_COLOR: LazyLock<bool> = LazyLock::new(|| {
|
||||||
env::var("NO_COLOR")
|
env::var("NO_COLOR")
|
||||||
.ok()
|
.ok()
|
||||||
|
|||||||
Reference in New Issue
Block a user