feat(tui): implement agent turn engine for background processing and enhance input handling
This commit is contained in:
@@ -78,8 +78,25 @@ impl Tool for BashKill {
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let _job_id = crate::tools::arg_str(args, "job_id")?;
|
||||
// In production, look up and kill the job in BashControl
|
||||
Ok(format!("Killed background job '{}'", _job_id))
|
||||
let job_id = crate::tools::arg_str(args, "job_id")?;
|
||||
info!("bash_kill called for job: {job_id}");
|
||||
// Try to kill by PID (if job_id is numeric) or by process name
|
||||
if let Ok(pid) = job_id.parse::<u32>() {
|
||||
use std::process::Command;
|
||||
match Command::new("kill").arg(pid.to_string()).output() {
|
||||
Ok(output) if output.status.success() => {
|
||||
Ok(format!("Killed background job '{job_id}' (PID {pid})"))
|
||||
}
|
||||
Ok(output) => {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
Ok(format!("Failed to kill job '{job_id}': {stderr}"))
|
||||
}
|
||||
Err(e) => {
|
||||
Ok(format!("Failed to kill job '{job_id}': {e}"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(format!("Invalid job ID '{job_id}' — expected numeric PID"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,13 @@ impl Tool for LspCompletion {
|
||||
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
|
||||
let manager = ctx.lsp_manager.lock().unwrap();
|
||||
let manager = match ctx.lsp_manager.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => {
|
||||
tracing::error!("LSP manager mutex poisoned, recovering");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/completion", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
|
||||
@@ -46,7 +46,13 @@ impl Tool for LspConnect {
|
||||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut manager = ctx.lsp_manager.lock().unwrap();
|
||||
let mut manager = match ctx.lsp_manager.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => {
|
||||
tracing::error!("LSP manager mutex poisoned, recovering");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
manager.start(&language, &command, &extra_args)?;
|
||||
|
||||
Ok(format!("Connected LSP for '{language}'"))
|
||||
|
||||
@@ -46,7 +46,13 @@ impl Tool for LspDefinition {
|
||||
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
|
||||
let manager = ctx.lsp_manager.lock().unwrap();
|
||||
let manager = match ctx.lsp_manager.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => {
|
||||
tracing::error!("LSP manager mutex poisoned, recovering");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/definition", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
|
||||
@@ -36,7 +36,13 @@ impl Tool for LspDiagnostics {
|
||||
let language = crate::tools::arg_str(args, "language")?;
|
||||
let path = crate::tools::arg_str(args, "path")?;
|
||||
|
||||
let manager = ctx.lsp_manager.lock().unwrap();
|
||||
let manager = match ctx.lsp_manager.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => {
|
||||
tracing::error!("LSP manager mutex poisoned, recovering");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/diagnostic", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) }
|
||||
|
||||
@@ -30,7 +30,13 @@ impl Tool for LspDisconnect {
|
||||
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let language = crate::tools::arg_str(args, "language")?;
|
||||
let _manager = ctx.lsp_manager.lock().unwrap();
|
||||
let _manager = match ctx.lsp_manager.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => {
|
||||
tracing::error!("LSP manager mutex poisoned, recovering");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
Ok(format!("Disconnected LSP for '{language}'"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,13 @@ impl Tool for LspHover {
|
||||
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
|
||||
let manager = ctx.lsp_manager.lock().unwrap();
|
||||
let manager = match ctx.lsp_manager.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => {
|
||||
tracing::error!("LSP manager mutex poisoned, recovering");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/hover", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
|
||||
@@ -46,7 +46,13 @@ impl Tool for LspReferences {
|
||||
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
|
||||
let manager = ctx.lsp_manager.lock().unwrap();
|
||||
let manager = match ctx.lsp_manager.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => {
|
||||
tracing::error!("LSP manager mutex poisoned, recovering");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/references", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::info;
|
||||
|
||||
pub struct PlanEnter;
|
||||
|
||||
@@ -51,11 +52,28 @@ impl Tool for PlanReady {
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
"properties": {
|
||||
"plan": {
|
||||
"type": "string",
|
||||
"description": "The final plan content"
|
||||
}
|
||||
},
|
||||
"required": ["plan"]
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &ToolCtx, _args: &Value) -> Result<String> {
|
||||
Ok("Plan is ready. Starting execution.".to_string())
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let plan_content = crate::tools::arg_str(args, "plan")?;
|
||||
info!("plan ready: {} chars", plan_content.len());
|
||||
// Persist the plan to session directory for reference
|
||||
let plan_dir = ctx.session_dir.join("plans");
|
||||
if std::fs::create_dir_all(&plan_dir).is_ok() {
|
||||
let filename = format!("plan-{}.md", chrono::Utc::now().format("%Y%m%d_%H%M%S"));
|
||||
let path = plan_dir.join(&filename);
|
||||
let _ = std::fs::write(&path, &plan_content);
|
||||
Ok(format!("Plan saved to {filename}. Starting execution."))
|
||||
} else {
|
||||
Ok("Plan is ready. Starting execution.".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ pub fn is_credential_path(path: &str) -> bool {
|
||||
|
||||
/// Check whether a command reads credential files.
|
||||
pub fn check_credential_read(cmd: &str) -> Vec<String> {
|
||||
let re = Regex::new(r#"(?i)(?:cat|head|tail|less|more|vim?|nano|xdg-open|open|type|echo)\s+(~?/[\w/.@-]+)"#).unwrap();
|
||||
let re = Regex::new(r#"(?i)(?:cat|head|tail|less|more|vim?|nano|xdg-open|open|type|echo)\s+(~?/[\w/.@-]+)"#)
|
||||
.expect("hardcoded credential-read regex is valid");
|
||||
let mut findings = Vec::new();
|
||||
for cap in re.captures_iter(cmd) {
|
||||
let path = cap.get(1).map(|m| m.as_str()).unwrap_or("");
|
||||
|
||||
Reference in New Issue
Block a user