Refactor scrolling methods in ScrollState to accept an amount parameter

- Updated `scroll_up` and `scroll_down` methods to take an `amount` parameter for more flexible scrolling.
- Removed the `AgentMode` enum and related methods from the types module to simplify state management.
- Modified `AppStateRest` to remove the `mode` field and adjusted related logic.
- Enhanced `run_subagent` to build tool definitions and handle API key resolution from configuration.
- Updated command parsing to reflect changes in login handling.
- Removed onboarding overlays and related logic from input handling and rendering.
- Improved status bar to reflect connection status and agent readiness.
- Adjusted workflow panel rendering to simplify phase status display.
- Refactored edit log initialization to load from disk if available.
- Updated settings structure to use a HashMap for API keys.
- Enhanced error handling in LlmClient for authentication issues.
This commit is contained in:
asepharyana
2026-07-12 03:14:52 +07:00
parent 71d3494372
commit 36573e7e3b
28 changed files with 435 additions and 482 deletions
+55 -15
View File
@@ -1,11 +1,26 @@
use serde_json::{json, Value};
use serde::{Deserialize, Serialize};
use std::io::{BufRead, BufReader, Write};
use std::sync::{Arc, Mutex, OnceLock};
const MCP_CONNECT_TIMEOUT_MS: u64 = 20_000;
const MCP_CALL_TIMEOUT_MS: u64 = 60_000;
/// Global cache for `&'static str` names/descriptions of MCP tools, so we
/// never need `Box::leak`. Entries are never removed (small, bounded by the
/// number of MCP tools ever registered in a session).
fn mcp_static_str(s: &str) -> &'static str {
static CACHE: OnceLock<Mutex<Vec<&'static str>>> = OnceLock::new();
let mut cache = CACHE.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap();
if let Some(existing) = cache.iter().find(|e| **e == s) {
return *existing;
}
let leaked: &'static str = Box::leak(s.to_string().into_boxed_str());
cache.push(leaked);
leaked
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum McpTransport {
Stdio {
@@ -29,17 +44,22 @@ pub struct McpServer {
pub name: String,
pub transport: McpTransport,
pub tools: Vec<McpToolInfo>,
/// Held child-process handle so subsequent tool calls reuse the same
/// connection instead of spawning a new child each time. Not serialized
/// because the child only lives in this process.
#[serde(skip)]
pub child_handle: Option<Arc<Mutex<StdioChild>>>,
}
#[derive(Debug)]
struct StdioChild {
pub struct StdioChild {
stdin: std::process::ChildStdin,
stdout: BufReader<std::process::ChildStdout>,
next_id: u64,
}
impl StdioChild {
fn call(&mut self, method: &str, params: Value) -> anyhow::Result<Value> {
pub fn call(&mut self, method: &str, params: Value) -> anyhow::Result<Value> {
self.next_id += 1;
let id = self.next_id;
let req = json!({
@@ -83,7 +103,7 @@ impl StdioChild {
}
}
fn spawn_stdio_child(command: &str, extra_args: &[String]) -> anyhow::Result<StdioChild> {
pub(crate) fn spawn_stdio_child(command: &str, extra_args: &[String]) -> anyhow::Result<StdioChild> {
let parts: Vec<&str> = command.split_whitespace().collect();
let (prog, prog_args) = parts.split_first()
.ok_or_else(|| anyhow::anyhow!("MCP stdio command is empty"))?;
@@ -132,8 +152,27 @@ fn spawn_stdio_child(command: &str, extra_args: &[String]) -> anyhow::Result<Std
Ok(mcp)
}
fn call_via_stdio(command: &str, extra_args: &[String], tool_name: &str, tool_args: &Value) -> anyhow::Result<String> {
let mut child = spawn_stdio_child(command, extra_args)?;
fn call_via_stdio(
existing_handle: Option<&Mutex<StdioChild>>,
command: &str,
extra_args: &[String],
tool_name: &str,
tool_args: &Value,
) -> anyhow::Result<String> {
// Reuse the persistent child handle if available; otherwise spawn a new one.
let mut guard;
let child: &mut StdioChild = if let Some(mtx) = existing_handle {
guard = mtx.lock().map_err(|e| anyhow::anyhow!("MCP handle lock: {}", e))?;
&mut *guard
} else {
let mut fresh = spawn_stdio_child(command, extra_args)?;
let result = fresh.call("tools/call", json!({
"name": tool_name,
"arguments": tool_args
}))?;
return extract_text_content(&result);
};
let result = child.call("tools/call", json!({
"name": tool_name,
"arguments": tool_args
@@ -212,15 +251,17 @@ pub struct McpToolAdapter {
pub transport: McpTransport,
pub description: String,
pub parameters: Value,
/// Shared handle to a persistent child process (stdio transport only).
pub child_handle: Option<Arc<Mutex<StdioChild>>>,
}
impl crate::tool::Tool for McpToolAdapter {
fn name(&self) -> &'static str {
Box::leak(format!("mcp__{}__{}", self.server_name, self.tool_name).into_boxed_str())
mcp_static_str(&format!("mcp__{}__{}", self.server_name, self.tool_name))
}
fn description(&self) -> &'static str {
Box::leak(self.description.clone().into_boxed_str())
mcp_static_str(&self.description)
}
fn parameters(&self) -> Value {
@@ -230,7 +271,7 @@ impl crate::tool::Tool for McpToolAdapter {
fn run(&self, _ctx: &crate::tool::ToolCtx, args: &Value) -> anyhow::Result<String> {
match &self.transport {
McpTransport::Stdio { command, args: extra_args } => {
call_via_stdio(command, extra_args, &self.tool_name, args)
call_via_stdio(self.child_handle.as_ref().map(|h| h.as_ref()), command, extra_args, &self.tool_name, args)
}
McpTransport::StreamableHttp { url } => {
call_via_http(url, &self.tool_name, args)
@@ -248,13 +289,15 @@ impl McpManager {
pub fn as_tools(&self) -> Vec<Box<dyn crate::tool::Tool>> {
self.servers.iter().flat_map(|server| {
server.tools.iter().map(|info| {
let handle = server.child_handle.clone();
server.tools.iter().map(move |info| {
Box::new(McpToolAdapter {
tool_name: info.name.clone(),
server_name: server.name.clone(),
transport: server.transport.clone(),
description: info.description.clone(),
parameters: info.input_schema.clone(),
child_handle: handle.clone(),
}) as Box<dyn crate::tool::Tool>
})
}).collect()
@@ -285,18 +328,15 @@ impl McpManager {
Vec::new()
};
let handle = Arc::new(Mutex::new(child));
self.servers.push(McpServer {
name: name.to_string(),
transport,
tools,
child_handle: Some(handle),
});
// Keep `child` alive for the lifetime of the server by not dropping it here.
// For now we rely on `call_via_stdio` re-spawning since `StdioChild` is
// not easily persisted across tool calls without threading the handle through.
// A follow-up can store the handle alongside the server.
drop(child);
Ok(())
}