feat(tui): implement agent turn engine for background processing and enhance input handling

This commit is contained in:
asepharyana
2026-07-20 10:55:09 +07:00
parent da2ed6da25
commit 792695b65a
31 changed files with 603 additions and 153 deletions
+20 -3
View File
@@ -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) },
+7 -1
View File
@@ -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}'"))
}
}
+7 -1
View File
@@ -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) },
+21 -3
View File
@@ -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("");