From d78cafba930a0ebb877810cc8c101fcdd24a558c Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Thu, 26 Mar 2026 16:21:53 -0400 Subject: [PATCH] feat: terminal_type MCP tool + tab resize fix + close confirmation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit terminal_type MCP tool (19 tools total): - Fire-and-forget text input to any session - Optional press_enter flag (default: true) - No marker wrapping, no output capture - Use case: AI sends messages/commands without needing output back Tab resize fix: - Double requestAnimationFrame before fitAddon.fit() on tab switch - Container has real dimensions after browser layout pass - Also sends ssh_resize/pty_resize to backend with correct cols/rows - Fixes 6-8 char wide terminals after switching tabs Close confirmation: - beforeunload event shows browser "Leave page?" dialog - Only triggers if sessions are active - Synchronous — cannot hang the close like onCloseRequested did Co-Authored-By: Claude Opus 4.6 (1M context) --- src-tauri/src/bin/wraith_mcp_bridge.rs | 14 +++++++++++ src-tauri/src/mcp/server.rs | 24 +++++++++++++++++++ src/components/terminal/LocalTerminalView.vue | 15 ++++++++---- src/components/terminal/TerminalView.vue | 21 ++++++++++++---- src/layouts/MainLayout.vue | 8 +++++++ 5 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/bin/wraith_mcp_bridge.rs b/src-tauri/src/bin/wraith_mcp_bridge.rs index 48afee8..d5f6ce8 100644 --- a/src-tauri/src/bin/wraith_mcp_bridge.rs +++ b/src-tauri/src/bin/wraith_mcp_bridge.rs @@ -83,6 +83,19 @@ fn handle_tools_list(id: Value) -> JsonRpcResponse { id, result: Some(serde_json::json!({ "tools": [ + { + "name": "terminal_type", + "description": "Type text into a terminal session (like a human typing). Optionally presses Enter after. Use this to send messages or commands without output capture.", + "inputSchema": { + "type": "object", + "properties": { + "session_id": { "type": "string", "description": "The session ID to type into" }, + "text": { "type": "string", "description": "The text to type" }, + "press_enter": { "type": "boolean", "description": "Whether to press Enter after typing (default: true)" } + }, + "required": ["session_id", "text"] + } + }, { "name": "terminal_read", "description": "Read recent terminal output from an active SSH or PTY session (ANSI codes stripped)", @@ -251,6 +264,7 @@ fn call_wraith(port: u16, endpoint: &str, body: Value) -> Result fn handle_tool_call(id: Value, port: u16, tool_name: &str, args: &Value) -> JsonRpcResponse { let result = match tool_name { "list_sessions" => call_wraith(port, "/mcp/sessions", serde_json::json!({})), + "terminal_type" => call_wraith(port, "/mcp/terminal/type", args.clone()), "terminal_read" => call_wraith(port, "/mcp/terminal/read", args.clone()), "terminal_execute" => call_wraith(port, "/mcp/terminal/execute", args.clone()), "sftp_list" => call_wraith(port, "/mcp/sftp/list", args.clone()), diff --git a/src-tauri/src/mcp/server.rs b/src-tauri/src/mcp/server.rs index 334304d..0eb203a 100644 --- a/src-tauri/src/mcp/server.rs +++ b/src-tauri/src/mcp/server.rs @@ -52,6 +52,13 @@ struct SftpWriteRequest { content: String, } +#[derive(Deserialize)] +struct TerminalTypeRequest { + session_id: String, + text: String, + press_enter: Option, +} + #[derive(Deserialize)] struct TerminalExecuteRequest { session_id: String, @@ -154,6 +161,22 @@ async fn handle_screenshot( } } +async fn handle_terminal_type( + AxumState(state): AxumState>, + Json(req): Json, +) -> Json> { + let text = if req.press_enter.unwrap_or(true) { + format!("{}\n", req.text) + } else { + req.text.clone() + }; + + match state.ssh.write(&req.session_id, text.as_bytes()).await { + Ok(()) => ok_response("sent".to_string()), + Err(e) => err_response(e), + } +} + async fn handle_terminal_read( AxumState(state): AxumState>, Json(req): Json, @@ -350,6 +373,7 @@ pub async fn start_mcp_server( let app = Router::new() .route("/mcp/sessions", post(handle_list_sessions)) + .route("/mcp/terminal/type", post(handle_terminal_type)) .route("/mcp/terminal/read", post(handle_terminal_read)) .route("/mcp/terminal/execute", post(handle_terminal_execute)) .route("/mcp/screenshot", post(handle_screenshot)) diff --git a/src/components/terminal/LocalTerminalView.vue b/src/components/terminal/LocalTerminalView.vue index 5a9e867..79be014 100644 --- a/src/components/terminal/LocalTerminalView.vue +++ b/src/components/terminal/LocalTerminalView.vue @@ -50,10 +50,17 @@ watch( () => props.isActive, (active) => { if (active) { - setTimeout(() => { - fit(); - terminal.focus(); - }, 0); + requestAnimationFrame(() => { + requestAnimationFrame(() => { + fit(); + terminal.focus(); + invoke("pty_resize", { + sessionId: props.sessionId, + cols: terminal.cols, + rows: terminal.rows, + }).catch(() => {}); + }); + }); } }, ); diff --git a/src/components/terminal/TerminalView.vue b/src/components/terminal/TerminalView.vue index dd40253..d9d1dc1 100644 --- a/src/components/terminal/TerminalView.vue +++ b/src/components/terminal/TerminalView.vue @@ -60,6 +60,7 @@