From ba4ff04feab0b548f0bf107672db5bf950868bfe Mon Sep 17 00:00:00 2001 From: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Date: Sat, 1 Aug 2026 21:52:32 -0400 Subject: [PATCH] feat(desktop): wire terminal sessions to Tauri Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> --- desktop/src-tauri/Cargo.lock | 1 + desktop/src-tauri/Cargo.toml | 1 + .../crates/buzz-terminal/src/listener.rs | 29 + .../crates/buzz-terminal/src/shared.rs | 10 + .../crates/buzz-terminal/src/shell.rs | 6 + desktop/src-tauri/src/lib.rs | 10 + desktop/src-tauri/src/shutdown.rs | 4 + desktop/src-tauri/src/terminal_runtime.rs | 782 ++++++++++++++++++ desktop/src-tauri/src/terminal_transport.rs | 14 + 9 files changed, 857 insertions(+) create mode 100644 desktop/src-tauri/src/terminal_runtime.rs diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 82cdd3313..b28fd0716 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1106,6 +1106,7 @@ dependencies = [ "opus", "plist", "png 0.18.1", + "portable-pty", "regex", "reqwest 0.13.4", "rodio", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 5e30315b6..da7cda9b2 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -105,6 +105,7 @@ buzz_sdk_pkg = { package = "buzz-sdk", path = "../../crates/buzz-sdk" } buzz_agent_pkg = { package = "buzz-agent", path = "../../crates/buzz-agent" } buzz_voice_pkg = { package = "buzz-voice", path = "../../crates/buzz-voice" } buzz_terminal = { package = "buzz-terminal", path = "crates/buzz-terminal" } +portable-pty = "0.9" iroh = { version = "1.0.2", optional = true } mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.74.0", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"], optional = true } mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.74.0", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"], optional = true } diff --git a/desktop/src-tauri/crates/buzz-terminal/src/listener.rs b/desktop/src-tauri/crates/buzz-terminal/src/listener.rs index 202cc063e..ac9fcfd60 100644 --- a/desktop/src-tauri/crates/buzz-terminal/src/listener.rs +++ b/desktop/src-tauri/crates/buzz-terminal/src/listener.rs @@ -80,6 +80,35 @@ impl Listener { } } +/// Resolve an emulator action that can be answered without renderer state. +/// +/// Color queries deliberately return `None`: named/indexed colors resolve +/// against the live theme, which this crate does not own. +pub fn reply( + action: Action, + columns: u16, + rows: u16, + cell_width: u16, + cell_height: u16, +) -> Option { + match action { + Action::PtyWrite(text) => Some(text), + Action::SizeReply { format } => Some(format(WindowSize { + num_lines: rows, + num_cols: columns, + cell_width, + cell_height, + })), + // Palette values are renderer-owned. The transport must answer these + // only after it has a renderer palette, never invent one here. + Action::ColorReply { .. } + | Action::Title(_) + | Action::ResetTitle + | Action::Wakeup + | Action::Bell => None, + } +} + impl EventListener for Listener { fn send_event(&self, event: Event) { let action = match event { diff --git a/desktop/src-tauri/crates/buzz-terminal/src/shared.rs b/desktop/src-tauri/crates/buzz-terminal/src/shared.rs index c76d094e9..c83e36410 100644 --- a/desktop/src-tauri/crates/buzz-terminal/src/shared.rs +++ b/desktop/src-tauri/crates/buzz-terminal/src/shared.rs @@ -199,6 +199,16 @@ impl SharedTerminal { self.acquire(&self.renderer) } + /// Modes the renderer/input boundary needs to report alongside frames. + pub fn input_modes(&self) -> (bool, bool) { + let term = self.acquire(&self.renderer); + let mode = term.term().mode(); + ( + mode.contains(alacritty_terminal::term::TermMode::BRACKETED_PASTE), + mode.contains(alacritty_terminal::term::TermMode::FOCUS_IN_OUT), + ) + } + fn acquire(&self, meter: &AcquireMeter) -> MutexGuard<'_, Terminal> { let started = Instant::now(); let guard = self.term.lock(); diff --git a/desktop/src-tauri/crates/buzz-terminal/src/shell.rs b/desktop/src-tauri/crates/buzz-terminal/src/shell.rs index e09dcacc3..fcee20ca2 100644 --- a/desktop/src-tauri/crates/buzz-terminal/src/shell.rs +++ b/desktop/src-tauri/crates/buzz-terminal/src/shell.rs @@ -130,3 +130,9 @@ pub fn login_argv0(shell: &str) -> String { let basename = shell.rsplit('/').next().unwrap_or(shell); format!("-{basename}") } + +/// Resolve the command shell on Windows from `ComSpec`, falling back to cmd. +#[cfg(windows)] +pub fn resolve_shell(shell_env: Option<&str>) -> String { + shell_env.unwrap_or("cmd.exe").to_owned() +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 6f75dddce..b6627bca0 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -32,6 +32,7 @@ mod reset; mod secret_store; mod shutdown; mod templates; +mod terminal_runtime; #[cfg_attr(not(test), allow(dead_code))] mod terminal_transport; #[cfg(target_os = "macos")] @@ -370,6 +371,7 @@ pub fn run() { .manage(BuilderlabSession::default()) .manage(BuilderlabLogin::default()) .manage(commands::pairing::PairingHandle::new()) + .manage(terminal_runtime::TerminalSessions::default()) .setup(move |app| { let app_handle = app.handle().clone(); #[cfg(target_os = "macos")] @@ -665,6 +667,14 @@ pub fn run() { Ok(()) }) .invoke_handler(tauri::generate_handler![ + terminal_runtime::terminal_attach, + terminal_runtime::terminal_detach, + terminal_runtime::terminal_close, + terminal_runtime::terminal_input, + terminal_runtime::terminal_resize, + terminal_runtime::terminal_ack, + terminal_runtime::terminal_viewport_ready, + terminal_runtime::terminal_focus, take_pending_community_deep_link, acknowledge_pending_community_deep_link, start_builderlab_login, diff --git a/desktop/src-tauri/src/shutdown.rs b/desktop/src-tauri/src/shutdown.rs index 95f9efc3c..efd88f3ca 100644 --- a/desktop/src-tauri/src/shutdown.rs +++ b/desktop/src-tauri/src/shutdown.rs @@ -19,6 +19,8 @@ pub(crate) fn shut_down_app(app: &tauri::AppHandle, shutdown_done: &std::sync::a .store(true, Ordering::SeqCst); if !shutdown_done.swap(true, Ordering::SeqCst) { prevent_sleep::release(&app.state::().prevent_sleep); + app.state::() + .shutdown_all(); if let Err(error) = shutdown_managed_agents(app) { eprintln!("buzz-desktop: failed to stop managed agents: {error}"); } @@ -40,6 +42,8 @@ pub(crate) fn install_signal_handler( .shutdown_started .store(true, Ordering::SeqCst); if !shutdown_done.swap(true, Ordering::SeqCst) { + app.state::() + .shutdown_all(); let _ = shutdown_managed_agents(&app); #[cfg(feature = "mesh-llm")] shutdown_mesh_runtime(&app); diff --git a/desktop/src-tauri/src/terminal_runtime.rs b/desktop/src-tauri/src/terminal_runtime.rs new file mode 100644 index 000000000..f82d7e3e2 --- /dev/null +++ b/desktop/src-tauri/src/terminal_runtime.rs @@ -0,0 +1,782 @@ +//! Rust-owned PTY sessions and the typed Tauri transport for Buzz Substrate. + +use std::io::{Read, Write}; +use std::sync::{Arc, Mutex}; +use std::thread::JoinHandle; + +use buzz_terminal::context::{context_vars, GuiContext}; +use buzz_terminal::damage::Style; +use buzz_terminal::{Fences, SharedTerminal, Size, Terminal, Viewport}; +use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize}; +use serde::{Deserialize, Serialize}; +use tauri::ipc::Channel; +use uuid::Uuid; + +use crate::terminal_transport::{FramePublisher, Publication, SubscriptionId}; + +const MAX_LIVE_SESSIONS: usize = 20; +const MAX_INPUT_BYTES: usize = 1024 * 1024; + +type Result = std::result::Result; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AttachRequest { + /// Present when a renderer remounts onto an existing PTY-backed tab. + session_id: Option, + channel_id: String, + channel_name: String, + thread_id: Option, + npub: String, + relay_url: String, + columns: u16, + rows: u16, + pixel_width: u16, + pixel_height: u16, +} + +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct WireViewport { + generation: u64, + columns: usize, + screen_lines: usize, +} + +impl From for WireViewport { + fn from(value: Viewport) -> Self { + Self { + generation: value.generation, + columns: value.columns, + screen_lines: value.screen_lines, + } + } +} + +impl From for Viewport { + fn from(value: WireViewport) -> Self { + Self { + generation: value.generation, + columns: value.columns, + screen_lines: value.screen_lines, + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AttachResponse { + session_id: String, + subscription_id: String, + viewport: WireViewport, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct WireStyle { + fg: u32, + bg: u32, + flags: u16, +} + +impl From