     1→//! Agent configuration management
     2→
     3→use anyhow::{Context, Result};
     4→use serde::{Deserialize, Serialize};
     5→use std::path::PathBuf;
     6→use uuid::Uuid;
     7→
     8→/// Agent configuration
     9→#[derive(Debug, Clone, Serialize, Deserialize)]
    10→pub struct Config {
    11→    /// Server WebSocket URL (e.g., wss://connect.example.com/ws)
    12→    pub server_url: String,
    13→
    14→    /// Agent API key for authentication
    15→    pub api_key: String,
    16→
    17→    /// Unique agent identifier (generated on first run)
    18→    #[serde(default = "generate_agent_id")]
    19→    pub agent_id: String,
    20→
    21→    /// Optional hostname override
    22→    pub hostname_override: Option<String>,
    23→
    24→    /// Support code for one-time support sessions (set via command line)
    25→    #[serde(skip)]
    26→    pub support_code: Option<String>,
    27→
    28→    /// Capture settings
    29→    #[serde(default)]
    30→    pub capture: CaptureConfig,
    31→
    32→    /// Encoding settings
    33→    #[serde(default)]
    34→    pub encoding: EncodingConfig,
    35→}
    36→
    37→fn generate_agent_id() -> String {
    38→    Uuid::new_v4().to_string()
    39→}
    40→
    41→#[derive(Debug, Clone, Serialize, Deserialize)]
    42→pub struct CaptureConfig {
    43→    /// Target frames per second (1-60)
    44→    #[serde(default = "default_fps")]
    45→    pub fps: u32,
    46→
    47→    /// Use DXGI Desktop Duplication (recommended)
    48→    #[serde(default = "default_true")]
    49→    pub use_dxgi: bool,
    50→
    51→    /// Fall back to GDI if DXGI fails
    52→    #[serde(default = "default_true")]
    53→    pub gdi_fallback: bool,
    54→}
    55→
    56→#[derive(Debug, Clone, Serialize, Deserialize)]
    57→pub struct EncodingConfig {
    58→    /// Preferred codec (auto, raw, vp9, h264)
    59→    #[serde(default = "default_codec")]
    60→    pub codec: String,
    61→
    62→    /// Quality (1-100, higher = better quality, more bandwidth)
    63→    #[serde(default = "default_quality")]
    64→    pub quality: u32,
    65→
    66→    /// Use hardware encoding if available
    67→    #[serde(default = "default_true")]
    68→    pub hardware_encoding: bool,
    69→}
    70→
    71→fn default_fps() -> u32 {
    72→    30
    73→}
    74→
    75→fn default_true() -> bool {
    76→    true
    77→}
    78→
    79→fn default_codec() -> String {
    80→    "auto".to_string()
    81→}
    82→
    83→fn default_quality() -> u32 {
    84→    75
    85→}
    86→
    87→impl Default for CaptureConfig {
    88→    fn default() -> Self {
    89→        Self {
    90→            fps: default_fps(),
    91→            use_dxgi: true,
    92→            gdi_fallback: true,
    93→        }
    94→    }
    95→}
    96→
    97→impl Default for EncodingConfig {
    98→    fn default() -> Self {
    99→        Self {
   100→            codec: default_codec(),
   101→            quality: default_quality(),
   102→            hardware_encoding: true,
   103→        }
   104→    }
   105→}
   106→
   107→impl Config {
   108→    /// Load configuration from file or environment
   109→    pub fn load() -> Result<Self> {
   110→        // Try loading from config file
   111→        let config_path = Self::config_path();
   112→
   113→        if config_path.exists() {
   114→            let contents = std::fs::read_to_string(&config_path)
   115→                .with_context(|| format!("Failed to read config from {:?}", config_path))?;
   116→
   117→            let mut config: Config = toml::from_str(&contents)
   118→                .with_context(|| "Failed to parse config file")?;
   119→
   120→            // Ensure agent_id is set and saved
   121→            if config.agent_id.is_empty() {
   122→                config.agent_id = generate_agent_id();
   123→                let _ = config.save();
   124→            }
   125→
   126→            // support_code is always None when loading from file (set via CLI)
   127→            config.support_code = None;
   128→
   129→            return Ok(config);
   130→        }
   131→
   132→        // Fall back to environment variables
   133→        let server_url = std::env::var("GURUCONNECT_SERVER_URL")
   134→            .unwrap_or_else(|_| "wss://connect.azcomputerguru.com/ws/agent".to_string());
   135→
   136→        let api_key = std::env::var("GURUCONNECT_API_KEY")
   137→            .unwrap_or_else(|_| "dev-key".to_string());
   138→
   139→        let agent_id = std::env::var("GURUCONNECT_AGENT_ID")
   140→            .unwrap_or_else(|_| generate_agent_id());
   141→
   142→        let config = Config {
   143→            server_url,
   144→            api_key,
   145→            agent_id,
   146→            hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(),
   147→            support_code: None, // Set via CLI
   148→            capture: CaptureConfig::default(),
   149→            encoding: EncodingConfig::default(),
   150→        };
   151→
   152→        // Save config with generated agent_id for persistence
   153→        let _ = config.save();
   154→
   155→        Ok(config)
   156→    }
   157→
   158→    /// Get the configuration file path
   159→    fn config_path() -> PathBuf {
   160→        // Check for config in current directory first
   161→        let local_config = PathBuf::from("guruconnect.toml");
   162→        if local_config.exists() {
   163→            return local_config;
   164→        }
   165→
   166→        // Check in program data directory (Windows)
   167→        #[cfg(windows)]
   168→        {
   169→            if let Ok(program_data) = std::env::var("ProgramData") {
   170→                let path = PathBuf::from(program_data)
   171→                    .join("GuruConnect")
   172→                    .join("agent.toml");
   173→                if path.exists() {
   174→                    return path;
   175→                }
   176→            }
   177→        }
   178→
   179→        // Default to local config
   180→        local_config
   181→    }
   182→
   183→    /// Get the hostname to use
   184→    pub fn hostname(&self) -> String {
   185→        self.hostname_override
   186→            .clone()
   187→            .unwrap_or_else(|| {
   188→                hostname::get()
   189→                    .map(|h| h.to_string_lossy().to_string())
   190→                    .unwrap_or_else(|_| "unknown".to_string())
   191→            })
   192→    }
   193→
   194→    /// Save current configuration to file
   195→    pub fn save(&self) -> Result<()> {
   196→        let config_path = Self::config_path();
   197→
   198→        // Ensure parent directory exists
   199→        if let Some(parent) = config_path.parent() {
   200→            std::fs::create_dir_all(parent)?;
   201→        }
   202→
   203→        let contents = toml::to_string_pretty(self)?;
   204→        std::fs::write(&config_path, contents)?;
   205→
   206→        Ok(())
   207→    }
   208→}
   209→
   210→/// Example configuration file content
   211→pub fn example_config() -> &'static str {
   212→    r#"# GuruConnect Agent Configuration
   213→
   214→# Server connection
   215→server_url = "wss://connect.example.com/ws"
   216→api_key = "your-agent-api-key"
   217→agent_id = "auto-generated-uuid"
   218→
   219→# Optional: override hostname
   220→# hostname_override = "custom-hostname"
   221→
   222→[capture]
   223→fps = 30
   224→use_dxgi = true
   225→gdi_fallback = true
   226→
   227→[encoding]
   228→codec = "auto"      # auto, raw, vp9, h264
   229→quality = 75        # 1-100
   230→hardware_encoding = true
   231→"#
   232→}
   233→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! Agent configuration management

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use uuid::Uuid;

/// Agent configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    /// Server WebSocket URL (e.g., wss://connect.example.com/ws)
    pub server_url: String,

    /// Agent API key for authentication
    pub api_key: String,

    /// Unique agent identifier (generated on first run)
    #[serde(default = "generate_agent_id")]
    pub agent_id: String,

    /// Optional hostname override
    pub hostname_override: Option<String>,

    /// Support code for one-time support sessions (set via command line)
    #[serde(skip)]
    pub support_code: Option<String>,

    /// Capture settings
    #[serde(default)]
    pub capture: CaptureConfig,

    /// Encoding settings
    #[serde(default)]
    pub encoding: EncodingConfig,
}

fn generate_agent_id() -> String {
    Uuid::new_v4().to_string()
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CaptureConfig {
    /// Target frames per second (1-60)
    #[serde(default = "default_fps")]
    pub fps: u32,

    /// Use DXGI Desktop Duplication (recommended)
    #[serde(default = "default_true")]
    pub use_dxgi: bool,

    /// Fall back to GDI if DXGI fails
    #[serde(default = "default_true")]
    pub gdi_fallback: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodingConfig {
    /// Preferred codec (auto, raw, vp9, h264)
    #[serde(default = "default_codec")]
    pub codec: String,

    /// Quality (1-100, higher = better quality, more bandwidth)
    #[serde(default = "default_quality")]
    pub quality: u32,

    /// Use hardware encoding if available
    #[serde(default = "default_true")]
    pub hardware_encoding: bool,
}

fn default_fps() -> u32 {
    30
}

fn default_true() -> bool {
    true
}

fn default_codec() -> String {
    "auto".to_string()
}

fn default_quality() -> u32 {
    75
}

impl Default for CaptureConfig {
    fn default() -> Self {
        Self {
            fps: default_fps(),
            use_dxgi: true,
            gdi_fallback: true,
        }
    }
}

impl Default for EncodingConfig {
    fn default() -> Self {
        Self {
            codec: default_codec(),
            quality: default_quality(),
            hardware_encoding: true,
        }
    }
}

impl Config {
    /// Load configuration from file or environment
    pub fn load() -> Result<Self> {
        // Try loading from config file
        let config_path = Self::config_path();

        if config_path.exists() {
            let contents = std::fs::read_to_string(&config_path)
                .with_context(|| format!("Failed to read config from {:?}", config_path))?;

            let mut config: Config = toml::from_str(&contents)
                .with_context(|| "Failed to parse config file")?;

            // Ensure agent_id is set and saved
            if config.agent_id.is_empty() {
                config.agent_id = generate_agent_id();
                let _ = config.save();
            }

            // support_code is always None when loading from file (set via CLI)
            config.support_code = None;

            return Ok(config);
        }

        // Fall back to environment variables
        let server_url = std::env::var("GURUCONNECT_SERVER_URL")
            .unwrap_or_else(|_| "wss://connect.azcomputerguru.com/ws/agent".to_string());

        let api_key = std::env::var("GURUCONNECT_API_KEY")
            .unwrap_or_else(|_| "dev-key".to_string());

        let agent_id = std::env::var("GURUCONNECT_AGENT_ID")
            .unwrap_or_else(|_| generate_agent_id());

        let config = Config {
            server_url,
            api_key,
            agent_id,
            hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(),
            support_code: None, // Set via CLI
            capture: CaptureConfig::default(),
            encoding: EncodingConfig::default(),
        };

        // Save config with generated agent_id for persistence
        let _ = config.save();

        Ok(config)
    }

    /// Get the configuration file path
    fn config_path() -> PathBuf {
        // Check for config in current directory first
        let local_config = PathBuf::from("guruconnect.toml");
        if local_config.exists() {
            return local_config;
        }

        // Check in program data directory (Windows)
        #[cfg(windows)]
        {
            if let Ok(program_data) = std::env::var("ProgramData") {
                let path = PathBuf::from(program_data)
                    .join("GuruConnect")
                    .join("agent.toml");
                if path.exists() {
                    return path;
                }
            }
        }

        // Default to local config
        local_config
    }

    /// Get the hostname to use
    pub fn hostname(&self) -> String {
        self.hostname_override
            .clone()
            .unwrap_or_else(|| {
                hostname::get()
                    .map(|h| h.to_string_lossy().to_string())
                    .unwrap_or_else(|_| "unknown".to_string())
            })
    }

    /// Save current configuration to file
    pub fn save(&self) -> Result<()> {
        let config_path = Self::config_path();

        // Ensure parent directory exists
        if let Some(parent) = config_path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        let contents = toml::to_string_pretty(self)?;
        std::fs::write(&config_path, contents)?;

        Ok(())
    }
}

/// Example configuration file content
pub fn example_config() -> &'static str {
    r#"# GuruConnect Agent Configuration

# Server connection
server_url = "wss://connect.example.com/ws"
api_key = "your-agent-api-key"
agent_id = "auto-generated-uuid"

# Optional: override hostname
# hostname_override = "custom-hostname"

[capture]
fps = 30
use_dxgi = true
gdi_fallback = true

[encoding]
codec = "auto"      # auto, raw, vp9, h264
quality = 75        # 1-100
hardware_encoding = true
"#
}


Now let me examine the transport module and session module to understand server communication better:


     1→//! WebSocket transport for agent-server communication
     2→
     3→mod websocket;
     4→
     5→pub use websocket::WebSocketTransport;
     6→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! WebSocket transport for agent-server communication

mod websocket;

pub use websocket::WebSocketTransport;


     1→//! WebSocket client transport
     2→//!
     3→//! Handles WebSocket connection to the GuruConnect server with:
     4→//! - TLS encryption
     5→//! - Automatic reconnection
     6→//! - Protobuf message serialization
     7→
     8→use crate::proto::Message;
     9→use anyhow::{Context, Result};
    10→use bytes::Bytes;
    11→use futures_util::{SinkExt, StreamExt};
    12→use prost::Message as ProstMessage;
    13→use std::collections::VecDeque;
    14→use std::sync::Arc;
    15→use tokio::net::TcpStream;
    16→use tokio::sync::Mutex;
    17→use tokio_tungstenite::{
    18→    connect_async, tungstenite::protocol::Message as WsMessage, MaybeTlsStream, WebSocketStream,
    19→};
    20→
    21→type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
    22→
    23→/// WebSocket transport for server communication
    24→pub struct WebSocketTransport {
    25→    stream: Arc<Mutex<WsStream>>,
    26→    incoming: VecDeque<Message>,
    27→    connected: bool,
    28→}
    29→
    30→impl WebSocketTransport {
    31→    /// Connect to the server
    32→    pub async fn connect(
    33→        url: &str,
    34→        agent_id: &str,
    35→        api_key: &str,
    36→        hostname: Option<&str>,
    37→        support_code: Option<&str>,
    38→    ) -> Result<Self> {
    39→        // Build query parameters
    40→        let mut params = format!("agent_id={}&api_key={}", agent_id, api_key);
    41→
    42→        if let Some(hostname) = hostname {
    43→            params.push_str(&format!("&hostname={}", urlencoding::encode(hostname)));
    44→        }
    45→
    46→        if let Some(code) = support_code {
    47→            params.push_str(&format!("&support_code={}", code));
    48→        }
    49→
    50→        // Append parameters to URL
    51→        let url_with_params = if url.contains('?') {
    52→            format!("{}&{}", url, params)
    53→        } else {
    54→            format!("{}?{}", url, params)
    55→        };
    56→
    57→        tracing::info!("Connecting to {} as agent {}", url, agent_id);
    58→        if let Some(code) = support_code {
    59→            tracing::info!("Using support code: {}", code);
    60→        }
    61→
    62→        let (ws_stream, response) = connect_async(&url_with_params)
    63→            .await
    64→            .context("Failed to connect to WebSocket server")?;
    65→
    66→        tracing::info!("Connected, status: {}", response.status());
    67→
    68→        Ok(Self {
    69→            stream: Arc::new(Mutex::new(ws_stream)),
    70→            incoming: VecDeque::new(),
    71→            connected: true,
    72→        })
    73→    }
    74→
    75→    /// Send a protobuf message
    76→    pub async fn send(&mut self, msg: Message) -> Result<()> {
    77→        let mut stream = self.stream.lock().await;
    78→
    79→        // Serialize to protobuf binary
    80→        let mut buf = Vec::with_capacity(msg.encoded_len());
    81→        msg.encode(&mut buf)?;
    82→
    83→        // Send as binary WebSocket message
    84→        stream
    85→            .send(WsMessage::Binary(buf.into()))
    86→            .await
    87→            .context("Failed to send message")?;
    88→
    89→        Ok(())
    90→    }
    91→
    92→    /// Try to receive a message (non-blocking)
    93→    pub fn try_recv(&mut self) -> Result<Option<Message>> {
    94→        // Return buffered message if available
    95→        if let Some(msg) = self.incoming.pop_front() {
    96→            return Ok(Some(msg));
    97→        }
    98→
    99→        // Try to receive more messages
   100→        let stream = self.stream.clone();
   101→        let result = tokio::task::block_in_place(|| {
   102→            tokio::runtime::Handle::current().block_on(async {
   103→                let mut stream = stream.lock().await;
   104→
   105→                // Use try_next for non-blocking receive
   106→                match tokio::time::timeout(
   107→                    std::time::Duration::from_millis(1),
   108→                    stream.next(),
   109→                )
   110→                .await
   111→                {
   112→                    Ok(Some(Ok(ws_msg))) => Ok(Some(ws_msg)),
   113→                    Ok(Some(Err(e))) => Err(anyhow::anyhow!("WebSocket error: {}", e)),
   114→                    Ok(None) => {
   115→                        // Connection closed
   116→                        Ok(None)
   117→                    }
   118→                    Err(_) => {
   119→                        // Timeout - no message available
   120→                        Ok(None)
   121→                    }
   122→                }
   123→            })
   124→        });
   125→
   126→        match result? {
   127→            Some(ws_msg) => {
   128→                if let Some(msg) = self.parse_message(ws_msg)? {
   129→                    Ok(Some(msg))
   130→                } else {
   131→                    Ok(None)
   132→                }
   133→            }
   134→            None => Ok(None),
   135→        }
   136→    }
   137→
   138→    /// Receive a message (blocking)
   139→    pub async fn recv(&mut self) -> Result<Option<Message>> {
   140→        // Return buffered message if available
   141→        if let Some(msg) = self.incoming.pop_front() {
   142→            return Ok(Some(msg));
   143→        }
   144→
   145→        let result = {
   146→            let mut stream = self.stream.lock().await;
   147→            stream.next().await
   148→        };
   149→
   150→        match result {
   151→            Some(Ok(ws_msg)) => self.parse_message(ws_msg),
   152→            Some(Err(e)) => {
   153→                self.connected = false;
   154→                Err(anyhow::anyhow!("WebSocket error: {}", e))
   155→            }
   156→            None => {
   157→                self.connected = false;
   158→                Ok(None)
   159→            }
   160→        }
   161→    }
   162→
   163→    /// Parse a WebSocket message into a protobuf message
   164→    fn parse_message(&mut self, ws_msg: WsMessage) -> Result<Option<Message>> {
   165→        match ws_msg {
   166→            WsMessage::Binary(data) => {
   167→                let msg = Message::decode(Bytes::from(data))
   168→                    .context("Failed to decode protobuf message")?;
   169→                Ok(Some(msg))
   170→            }
   171→            WsMessage::Ping(data) => {
   172→                // Pong is sent automatically by tungstenite
   173→                tracing::trace!("Received ping");
   174→                Ok(None)
   175→            }
   176→            WsMessage::Pong(_) => {
   177→                tracing::trace!("Received pong");
   178→                Ok(None)
   179→            }
   180→            WsMessage::Close(frame) => {
   181→                tracing::info!("Connection closed: {:?}", frame);
   182→                self.connected = false;
   183→                Ok(None)
   184→            }
   185→            WsMessage::Text(text) => {
   186→                // We expect binary protobuf, but log text messages
   187→                tracing::warn!("Received unexpected text message: {}", text);
   188→                Ok(None)
   189→            }
   190→            _ => Ok(None),
   191→        }
   192→    }
   193→
   194→    /// Check if connected
   195→    pub fn is_connected(&self) -> bool {
   196→        self.connected
   197→    }
   198→
   199→    /// Close the connection
   200→    pub async fn close(&mut self) -> Result<()> {
   201→        let mut stream = self.stream.lock().await;
   202→        stream.close(None).await?;
   203→        self.connected = false;
   204→        Ok(())
   205→    }
   206→}
   207→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! WebSocket client transport
//!
//! Handles WebSocket connection to the GuruConnect server with:
//! - TLS encryption
//! - Automatic reconnection
//! - Protobuf message serialization

use crate::proto::Message;
use anyhow::{Context, Result};
use bytes::Bytes;
use futures_util::{SinkExt, StreamExt};
use prost::Message as ProstMessage;
use std::collections::VecDeque;
use std::sync::Arc;
use tokio::net::TcpStream;
use tokio::sync::Mutex;
use tokio_tungstenite::{
    connect_async, tungstenite::protocol::Message as WsMessage, MaybeTlsStream, WebSocketStream,
};

type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;

/// WebSocket transport for server communication
pub struct WebSocketTransport {
    stream: Arc<Mutex<WsStream>>,
    incoming: VecDeque<Message>,
    connected: bool,
}

impl WebSocketTransport {
    /// Connect to the server
    pub async fn connect(
        url: &str,
        agent_id: &str,
        api_key: &str,
        hostname: Option<&str>,
        support_code: Option<&str>,
    ) -> Result<Self> {
        // Build query parameters
        let mut params = format!("agent_id={}&api_key={}", agent_id, api_key);

        if let Some(hostname) = hostname {
            params.push_str(&format!("&hostname={}", urlencoding::encode(hostname)));
        }

        if let Some(code) = support_code {
            params.push_str(&format!("&support_code={}", code));
        }

        // Append parameters to URL
        let url_with_params = if url.contains('?') {
            format!("{}&{}", url, params)
        } else {
            format!("{}?{}", url, params)
        };

        tracing::info!("Connecting to {} as agent {}", url, agent_id);
        if let Some(code) = support_code {
            tracing::info!("Using support code: {}", code);
        }

        let (ws_stream, response) = connect_async(&url_with_params)
            .await
            .context("Failed to connect to WebSocket server")?;

        tracing::info!("Connected, status: {}", response.status());

        Ok(Self {
            stream: Arc::new(Mutex::new(ws_stream)),
            incoming: VecDeque::new(),
            connected: true,
        })
    }

    /// Send a protobuf message
    pub async fn send(&mut self, msg: Message) -> Result<()> {
        let mut stream = self.stream.lock().await;

        // Serialize to protobuf binary
        let mut buf = Vec::with_capacity(msg.encoded_len());
        msg.encode(&mut buf)?;

        // Send as binary WebSocket message
        stream
            .send(WsMessage::Binary(buf.into()))
            .await
            .context("Failed to send message")?;

        Ok(())
    }

    /// Try to receive a message (non-blocking)
    pub fn try_recv(&mut self) -> Result<Option<Message>> {
        // Return buffered message if available
        if let Some(msg) = self.incoming.pop_front() {
            return Ok(Some(msg));
        }

        // Try to receive more messages
        let stream = self.stream.clone();
        let result = tokio::task::block_in_place(|| {
            tokio::runtime::Handle::current().block_on(async {
                let mut stream = stream.lock().await;

                // Use try_next for non-blocking receive
                match tokio::time::timeout(
                    std::time::Duration::from_millis(1),
                    stream.next(),
                )
                .await
                {
                    Ok(Some(Ok(ws_msg))) => Ok(Some(ws_msg)),
                    Ok(Some(Err(e))) => Err(anyhow::anyhow!("WebSocket error: {}", e)),
                    Ok(None) => {
                        // Connection closed
                        Ok(None)
                    }
                    Err(_) => {
                        // Timeout - no message available
                        Ok(None)
                    }
                }
            })
        });

        match result? {
            Some(ws_msg) => {
                if let Some(msg) = self.parse_message(ws_msg)? {
                    Ok(Some(msg))
                } else {
                    Ok(None)
                }
            }
            None => Ok(None),
        }
    }

    /// Receive a message (blocking)
    pub async fn recv(&mut self) -> Result<Option<Message>> {
        // Return buffered message if available
        if let Some(msg) = self.incoming.pop_front() {
            return Ok(Some(msg));
        }

        let result = {
            let mut stream = self.stream.lock().await;
            stream.next().await
        };

        match result {
            Some(Ok(ws_msg)) => self.parse_message(ws_msg),
            Some(Err(e)) => {
                self.connected = false;
                Err(anyhow::anyhow!("WebSocket error: {}", e))
            }
            None => {
                self.connected = false;
                Ok(None)
            }
        }
    }

    /// Parse a WebSocket message into a protobuf message
    fn parse_message(&mut self, ws_msg: WsMessage) -> Result<Option<Message>> {
        match ws_msg {
            WsMessage::Binary(data) => {
                let msg = Message::decode(Bytes::from(data))
                    .context("Failed to decode protobuf message")?;
                Ok(Some(msg))
            }
            WsMessage::Ping(data) => {
                // Pong is sent automatically by tungstenite
                tracing::trace!("Received ping");
                Ok(None)
            }
            WsMessage::Pong(_) => {
                tracing::trace!("Received pong");
                Ok(None)
            }
            WsMessage::Close(frame) => {
                tracing::info!("Connection closed: {:?}", frame);
                self.connected = false;
                Ok(None)
            }
            WsMessage::Text(text) => {
                // We expect binary protobuf, but log text messages
                tracing::warn!("Received unexpected text message: {}", text);
                Ok(None)
            }
            _ => Ok(None),
        }
    }

    /// Check if connected
    pub fn is_connected(&self) -> bool {
        self.connected
    }

    /// Close the connection
    pub async fn close(&mut self) -> Result<()> {
        let mut stream = self.stream.lock().await;
        stream.close(None).await?;
        self.connected = false;
        Ok(())
    }
}


     1→//! Session management for the agent
     2→//!
     3→//! Handles the lifecycle of a remote session including:
     4→//! - Connection to server
     5→//! - Idle mode (heartbeat only, minimal resources)
     6→//! - Active/streaming mode (capture and send frames)
     7→//! - Input event handling
     8→
     9→#[cfg(windows)]
    10→use windows::Win32::System::Console::{AllocConsole, GetConsoleWindow};
    11→#[cfg(windows)]
    12→use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_SHOW};
    13→
    14→use crate::capture::{self, Capturer, Display};
    15→use crate::chat::{ChatController, ChatMessage as ChatMsg};
    16→use crate::config::Config;
    17→use crate::encoder::{self, Encoder};
    18→use crate::input::InputController;
    19→
    20→/// Show the debug console window (Windows only)
    21→#[cfg(windows)]
    22→fn show_debug_console() {
    23→    unsafe {
    24→        let hwnd = GetConsoleWindow();
    25→        if hwnd.0 == std::ptr::null_mut() {
    26→            let _ = AllocConsole();
    27→            tracing::info!("Debug console window opened");
    28→        } else {
    29→            let _ = ShowWindow(hwnd, SW_SHOW);
    30→            tracing::info!("Debug console window shown");
    31→        }
    32→    }
    33→}
    34→
    35→#[cfg(not(windows))]
    36→fn show_debug_console() {
    37→    // No-op on non-Windows platforms
    38→}
    39→
    40→use crate::proto::{Message, message, ChatMessage, AgentStatus, Heartbeat, HeartbeatAck};
    41→use crate::transport::WebSocketTransport;
    42→use crate::tray::{TrayController, TrayAction};
    43→use anyhow::Result;
    44→use std::time::{Duration, Instant};
    45→
    46→// Heartbeat interval (30 seconds)
    47→const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30);
    48→// Status report interval (60 seconds)
    49→const STATUS_INTERVAL: Duration = Duration::from_secs(60);
    50→
    51→/// Session manager handles the remote control session
    52→pub struct SessionManager {
    53→    config: Config,
    54→    transport: Option<WebSocketTransport>,
    55→    state: SessionState,
    56→    // Lazy-initialized streaming resources
    57→    capturer: Option<Box<dyn Capturer>>,
    58→    encoder: Option<Box<dyn Encoder>>,
    59→    input: Option<InputController>,
    60→    // Streaming state
    61→    current_viewer_id: Option<String>,
    62→    // System info for status reports
    63→    hostname: String,
    64→    is_elevated: bool,
    65→    start_time: Instant,
    66→}
    67→
    68→#[derive(Debug, Clone, PartialEq)]
    69→enum SessionState {
    70→    Disconnected,
    71→    Connecting,
    72→    Idle,       // Connected but not streaming - minimal resource usage
    73→    Streaming,  // Actively capturing and sending frames
    74→}
    75→
    76→impl SessionManager {
    77→    /// Create a new session manager
    78→    pub fn new(config: Config, is_elevated: bool) -> Self {
    79→        let hostname = config.hostname();
    80→        Self {
    81→            config,
    82→            transport: None,
    83→            state: SessionState::Disconnected,
    84→            capturer: None,
    85→            encoder: None,
    86→            input: None,
    87→            current_viewer_id: None,
    88→            hostname,
    89→            is_elevated,
    90→            start_time: Instant::now(),
    91→        }
    92→    }
    93→
    94→    /// Connect to the server
    95→    pub async fn connect(&mut self) -> Result<()> {
    96→        self.state = SessionState::Connecting;
    97→
    98→        let transport = WebSocketTransport::connect(
    99→            &self.config.server_url,
   100→            &self.config.agent_id,
   101→            &self.config.api_key,
   102→            Some(&self.hostname),
   103→            self.config.support_code.as_deref(),
   104→        ).await?;
   105→
   106→        self.transport = Some(transport);
   107→        self.state = SessionState::Idle;  // Start in idle mode
   108→
   109→        tracing::info!("Connected to server, entering idle mode");
   110→
   111→        Ok(())
   112→    }
   113→
   114→    /// Initialize streaming resources (capturer, encoder, input)
   115→    fn init_streaming(&mut self) -> Result<()> {
   116→        if self.capturer.is_some() {
   117→            return Ok(()); // Already initialized
   118→        }
   119→
   120→        tracing::info!("Initializing streaming resources...");
   121→
   122→        // Get primary display
   123→        let primary_display = capture::primary_display()?;
   124→        tracing::info!("Using display: {} ({}x{})",
   125→            primary_display.name, primary_display.width, primary_display.height);
   126→
   127→        // Create capturer
   128→        let capturer = capture::create_capturer(
   129→            primary_display.clone(),
   130→            self.config.capture.use_dxgi,
   131→            self.config.capture.gdi_fallback,
   132→        )?;
   133→        self.capturer = Some(capturer);
   134→
   135→        // Create encoder
   136→        let encoder = encoder::create_encoder(
   137→            &self.config.encoding.codec,
   138→            self.config.encoding.quality,
   139→        )?;
   140→        self.encoder = Some(encoder);
   141→
   142→        // Create input controller
   143→        let input = InputController::new()?;
   144→        self.input = Some(input);
   145→
   146→        tracing::info!("Streaming resources initialized");
   147→        Ok(())
   148→    }
   149→
   150→    /// Release streaming resources to save CPU/memory when idle
   151→    fn release_streaming(&mut self) {
   152→        if self.capturer.is_some() {
   153→            tracing::info!("Releasing streaming resources");
   154→            self.capturer = None;
   155→            self.encoder = None;
   156→            self.input = None;
   157→            self.current_viewer_id = None;
   158→        }
   159→    }
   160→
   161→    /// Get display count for status reports
   162→    fn get_display_count(&self) -> i32 {
   163→        capture::enumerate_displays().map(|d| d.len() as i32).unwrap_or(1)
   164→    }
   165→
   166→    /// Send agent status to server
   167→    async fn send_status(&mut self) -> Result<()> {
   168→        let status = AgentStatus {
   169→            hostname: self.hostname.clone(),
   170→            os_version: std::env::consts::OS.to_string(),
   171→            is_elevated: self.is_elevated,
   172→            uptime_secs: self.start_time.elapsed().as_secs() as i64,
   173→            display_count: self.get_display_count(),
   174→            is_streaming: self.state == SessionState::Streaming,
   175→        };
   176→
   177→        let msg = Message {
   178→            payload: Some(message::Payload::AgentStatus(status)),
   179→        };
   180→
   181→        if let Some(transport) = self.transport.as_mut() {
   182→            transport.send(msg).await?;
   183→        }
   184→
   185→        Ok(())
   186→    }
   187→
   188→    /// Send heartbeat to server
   189→    async fn send_heartbeat(&mut self) -> Result<()> {
   190→        let heartbeat = Heartbeat {
   191→            timestamp: chrono::Utc::now().timestamp_millis(),
   192→        };
   193→
   194→        let msg = Message {
   195→            payload: Some(message::Payload::Heartbeat(heartbeat)),
   196→        };
   197→
   198→        if let Some(transport) = self.transport.as_mut() {
   199→            transport.send(msg).await?;
   200→        }
   201→
   202→        Ok(())
   203→    }
   204→
   205→    /// Run the session main loop with tray and chat event processing
   206→    pub async fn run_with_tray(&mut self, tray: Option<&TrayController>, chat: Option<&ChatController>) -> Result<()> {
   207→        if self.transport.is_none() {
   208→            anyhow::bail!("Not connected");
   209→        }
   210→
   211→        // Send initial status
   212→        self.send_status().await?;
   213→
   214→        // Timing for heartbeat and status
   215→        let mut last_heartbeat = Instant::now();
   216→        let mut last_status = Instant::now();
   217→        let mut last_frame_time = Instant::now();
   218→        let frame_interval = Duration::from_millis(1000 / self.config.capture.fps as u64);
   219→
   220→        // Main loop
   221→        loop {
   222→            // Process tray events
   223→            if let Some(t) = tray {
   224→                if let Some(action) = t.process_events() {
   225→                    match action {
   226→                        TrayAction::EndSession => {
   227→                            tracing::info!("User requested session end via tray");
   228→                            return Err(anyhow::anyhow!("USER_EXIT: Session ended by user"));
   229→                        }
   230→                        TrayAction::ShowDetails => {
   231→                            tracing::info!("User requested details (not yet implemented)");
   232→                        }
   233→                        TrayAction::ShowDebugWindow => {
   234→                            show_debug_console();
   235→                        }
   236→                    }
   237→                }
   238→
   239→                if t.exit_requested() {
   240→                    tracing::info!("Exit requested via tray");
   241→                    return Err(anyhow::anyhow!("USER_EXIT: Exit requested by user"));
   242→                }
   243→            }
   244→
   245→            // Process incoming messages
   246→            let messages: Vec<Message> = {
   247→                let transport = self.transport.as_mut().unwrap();
   248→                let mut msgs = Vec::new();
   249→                while let Some(msg) = transport.try_recv()? {
   250→                    msgs.push(msg);
   251→                }
   252→                msgs
   253→            };
   254→
   255→            for msg in messages {
   256→                // Handle chat messages specially
   257→                if let Some(message::Payload::ChatMessage(chat_msg)) = &msg.payload {
   258→                    if let Some(c) = chat {
   259→                        c.add_message(ChatMsg {
   260→                            id: chat_msg.id.clone(),
   261→                            sender: chat_msg.sender.clone(),
   262→                            content: chat_msg.content.clone(),
   263→                            timestamp: chat_msg.timestamp,
   264→                        });
   265→                    }
   266→                    continue;
   267→                }
   268→
   269→                // Handle control messages that affect state
   270→                if let Some(ref payload) = msg.payload {
   271→                    match payload {
   272→                        message::Payload::StartStream(start) => {
   273→                            tracing::info!("StartStream received from viewer: {}", start.viewer_id);
   274→                            if let Err(e) = self.init_streaming() {
   275→                                tracing::error!("Failed to init streaming: {}", e);
   276→                            } else {
   277→                                self.state = SessionState::Streaming;
   278→                                self.current_viewer_id = Some(start.viewer_id.clone());
   279→                                tracing::info!("Now streaming to viewer {}", start.viewer_id);
   280→                            }
   281→                            continue;
   282→                        }
   283→                        message::Payload::StopStream(stop) => {
   284→                            tracing::info!("StopStream received for viewer: {}", stop.viewer_id);
   285→                            // Only stop if it matches current viewer
   286→                            if self.current_viewer_id.as_ref() == Some(&stop.viewer_id) {
   287→                                self.release_streaming();
   288→                                self.state = SessionState::Idle;
   289→                                tracing::info!("Stopped streaming, returning to idle mode");
   290→                            }
   291→                            continue;
   292→                        }
   293→                        message::Payload::Heartbeat(hb) => {
   294→                            // Respond to server heartbeat with ack
   295→                            let ack = HeartbeatAck {
   296→                                client_timestamp: hb.timestamp,
   297→                                server_timestamp: chrono::Utc::now().timestamp_millis(),
   298→                            };
   299→                            let ack_msg = Message {
   300→                                payload: Some(message::Payload::HeartbeatAck(ack)),
   301→                            };
   302→                            if let Some(transport) = self.transport.as_mut() {
   303→                                let _ = transport.send(ack_msg).await;
   304→                            }
   305→                            continue;
   306→                        }
   307→                        _ => {}
   308→                    }
   309→                }
   310→
   311→                // Handle other messages (input events, disconnect, etc.)
   312→                self.handle_message(msg)?;
   313→            }
   314→
   315→            // Check for outgoing chat messages
   316→            if let Some(c) = chat {
   317→                if let Some(outgoing) = c.poll_outgoing() {
   318→                    let chat_proto = ChatMessage {
   319→                        id: outgoing.id,
   320→                        sender: "client".to_string(),
   321→                        content: outgoing.content,
   322→                        timestamp: outgoing.timestamp,
   323→                    };
   324→                    let msg = Message {
   325→                        payload: Some(message::Payload::ChatMessage(chat_proto)),
   326→                    };
   327→                    let transport = self.transport.as_mut().unwrap();
   328→                    transport.send(msg).await?;
   329→                }
   330→            }
   331→
   332→            // State-specific behavior
   333→            match self.state {
   334→                SessionState::Idle => {
   335→                    // In idle mode, just send heartbeats and status periodically
   336→                    if last_heartbeat.elapsed() >= HEARTBEAT_INTERVAL {
   337→                        last_heartbeat = Instant::now();
   338→                        if let Err(e) = self.send_heartbeat().await {
   339→                            tracing::warn!("Failed to send heartbeat: {}", e);
   340→                        }
   341→                    }
   342→
   343→                    if last_status.elapsed() >= STATUS_INTERVAL {
   344→                        last_status = Instant::now();
   345→                        if let Err(e) = self.send_status().await {
   346→                            tracing::warn!("Failed to send status: {}", e);
   347→                        }
   348→                    }
   349→
   350→                    // Longer sleep in idle mode to reduce CPU usage
   351→                    tokio::time::sleep(Duration::from_millis(100)).await;
   352→                }
   353→                SessionState::Streaming => {
   354→                    // In streaming mode, capture and send frames
   355→                    if last_frame_time.elapsed() >= frame_interval {
   356→                        last_frame_time = Instant::now();
   357→
   358→                        if let (Some(capturer), Some(encoder)) =
   359→                            (self.capturer.as_mut(), self.encoder.as_mut())
   360→                        {
   361→                            if let Ok(Some(frame)) = capturer.capture() {
   362→                                if let Ok(encoded) = encoder.encode(&frame) {
   363→                                    if encoded.size > 0 {
   364→                                        let msg = Message {
   365→                                            payload: Some(message::Payload::VideoFrame(encoded.frame)),
   366→                                        };
   367→                                        let transport = self.transport.as_mut().unwrap();
   368→                                        if let Err(e) = transport.send(msg).await {
   369→                                            tracing::warn!("Failed to send frame: {}", e);
   370→                                        }
   371→                                    }
   372→                                }
   373→                            }
   374→                        }
   375→                    }
   376→
   377→                    // Short sleep in streaming mode
   378→                    tokio::time::sleep(Duration::from_millis(1)).await;
   379→                }
   380→                _ => {
   381→                    // Disconnected or connecting - shouldn't be in main loop
   382→                    tokio::time::sleep(Duration::from_millis(100)).await;
   383→                }
   384→            }
   385→
   386→            // Check if still connected
   387→            if let Some(transport) = self.transport.as_ref() {
   388→                if !transport.is_connected() {
   389→                    tracing::warn!("Connection lost");
   390→                    break;
   391→                }
   392→            } else {
   393→                tracing::warn!("Transport is None");
   394→                break;
   395→            }
   396→        }
   397→
   398→        self.release_streaming();
   399→        self.state = SessionState::Disconnected;
   400→        Ok(())
   401→    }
   402→
   403→    /// Handle incoming message from server
   404→    fn handle_message(&mut self, msg: Message) -> Result<()> {
   405→        match msg.payload {
   406→            Some(message::Payload::MouseEvent(mouse)) => {
   407→                if let Some(input) = self.input.as_mut() {
   408→                    use crate::proto::MouseEventType;
   409→                    use crate::input::MouseButton;
   410→
   411→                    match MouseEventType::try_from(mouse.event_type).unwrap_or(MouseEventType::MouseMove) {
   412→                        MouseEventType::MouseMove => {
   413→                            input.mouse_move(mouse.x, mouse.y)?;
   414→                        }
   415→                        MouseEventType::MouseDown => {
   416→                            input.mouse_move(mouse.x, mouse.y)?;
   417→                            if let Some(ref buttons) = mouse.buttons {
   418→                                if buttons.left { input.mouse_click(MouseButton::Left, true)?; }
   419→                                if buttons.right { input.mouse_click(MouseButton::Right, true)?; }
   420→                                if buttons.middle { input.mouse_click(MouseButton::Middle, true)?; }
   421→                            }
   422→                        }
   423→                        MouseEventType::MouseUp => {
   424→                            if let Some(ref buttons) = mouse.buttons {
   425→                                if buttons.left { input.mouse_click(MouseButton::Left, false)?; }
   426→                                if buttons.right { input.mouse_click(MouseButton::Right, false)?; }
   427→                                if buttons.middle { input.mouse_click(MouseButton::Middle, false)?; }
   428→                            }
   429→                        }
   430→                        MouseEventType::MouseWheel => {
   431→                            input.mouse_scroll(mouse.wheel_delta_x, mouse.wheel_delta_y)?;
   432→                        }
   433→                    }
   434→                }
   435→            }
   436→
   437→            Some(message::Payload::KeyEvent(key)) => {
   438→                if let Some(input) = self.input.as_mut() {
   439→                    input.key_event(key.vk_code as u16, key.down)?;
   440→                }
   441→            }
   442→
   443→            Some(message::Payload::SpecialKey(special)) => {
   444→                if let Some(input) = self.input.as_mut() {
   445→                    use crate::proto::SpecialKey;
   446→                    match SpecialKey::try_from(special.key).ok() {
   447→                        Some(SpecialKey::CtrlAltDel) => {
   448→                            input.send_ctrl_alt_del()?;
   449→                        }
   450→                        _ => {}
   451→                    }
   452→                }
   453→            }
   454→
   455→            Some(message::Payload::AdminCommand(cmd)) => {
   456→                use crate::proto::AdminCommandType;
   457→                tracing::info!("Admin command received: {:?} - {}", cmd.command, cmd.reason);
   458→
   459→                match AdminCommandType::try_from(cmd.command).ok() {
   460→                    Some(AdminCommandType::AdminUninstall) => {
   461→                        tracing::warn!("Uninstall command received from server");
   462→                        // Return special error to trigger uninstall in main loop
   463→                        return Err(anyhow::anyhow!("ADMIN_UNINSTALL: {}", cmd.reason));
   464→                    }
   465→                    Some(AdminCommandType::AdminRestart) => {
   466→                        tracing::info!("Restart command received from server");
   467→                        // For now, just disconnect - the auto-restart logic will handle it
   468→                        return Err(anyhow::anyhow!("ADMIN_RESTART: {}", cmd.reason));
   469→                    }
   470→                    Some(AdminCommandType::AdminUpdate) => {
   471→                        tracing::info!("Update command received (not implemented)");
   472→                    }
   473→                    None => {
   474→                        tracing::warn!("Unknown admin command: {}", cmd.command);
   475→                    }
   476→                }
   477→            }
   478→
   479→            Some(message::Payload::Disconnect(disc)) => {
   480→                tracing::info!("Disconnect requested: {}", disc.reason);
   481→                if disc.reason.contains("cancelled") {
   482→                    return Err(anyhow::anyhow!("SESSION_CANCELLED: {}", disc.reason));
   483→                }
   484→                if disc.reason.contains("administrator") || disc.reason.contains("Disconnected") {
   485→                    return Err(anyhow::anyhow!("ADMIN_DISCONNECT: {}", disc.reason));
   486→                }
   487→                return Err(anyhow::anyhow!("Disconnect: {}", disc.reason));
   488→            }
   489→
   490→            _ => {
   491→                // Ignore unknown messages
   492→            }
   493→        }
   494→
   495→        Ok(())
   496→    }
   497→}
   498→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! Session management for the agent
//!
//! Handles the lifecycle of a remote session including:
//! - Connection to server
//! - Idle mode (heartbeat only, minimal resources)
//! - Active/streaming mode (capture and send frames)
//! - Input event handling

#[cfg(windows)]
use windows::Win32::System::Console::{AllocConsole, GetConsoleWindow};
#[cfg(windows)]
use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_SHOW};

use crate::capture::{self, Capturer, Display};
use crate::chat::{ChatController, ChatMessage as ChatMsg};
use crate::config::Config;
use crate::encoder::{self, Encoder};
use crate::input::InputController;

/// Show the debug console window (Windows only)
#[cfg(windows)]
fn show_debug_console() {
    unsafe {
        let hwnd = GetConsoleWindow();
        if hwnd.0 == std::ptr::null_mut() {
            let _ = AllocConsole();
            tracing::info!("Debug console window opened");
        } else {
            let _ = ShowWindow(hwnd, SW_SHOW);
            tracing::info!("Debug console window shown");
        }
    }
}

#[cfg(not(windows))]
fn show_debug_console() {
    // No-op on non-Windows platforms
}

use crate::proto::{Message, message, ChatMessage, AgentStatus, Heartbeat, HeartbeatAck};
use crate::transport::WebSocketTransport;
use crate::tray::{TrayController, TrayAction};
use anyhow::Result;
use std::time::{Duration, Instant};

// Heartbeat interval (30 seconds)
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30);
// Status report interval (60 seconds)
const STATUS_INTERVAL: Duration = Duration::from_secs(60);

/// Session manager handles the remote control session
pub struct SessionManager {
    config: Config,
    transport: Option<WebSocketTransport>,
    state: SessionState,
    // Lazy-initialized streaming resources
    capturer: Option<Box<dyn Capturer>>,
    encoder: Option<Box<dyn Encoder>>,
    input: Option<InputController>,
    // Streaming state
    current_viewer_id: Option<String>,
    // System info for status reports
    hostname: String,
    is_elevated: bool,
    start_time: Instant,
}

#[derive(Debug, Clone, PartialEq)]
enum SessionState {
    Disconnected,
    Connecting,
    Idle,       // Connected but not streaming - minimal resource usage
    Streaming,  // Actively capturing and sending frames
}

impl SessionManager {
    /// Create a new session manager
    pub fn new(config: Config, is_elevated: bool) -> Self {
        let hostname = config.hostname();
        Self {
            config,
            transport: None,
            state: SessionState::Disconnected,
            capturer: None,
            encoder: None,
            input: None,
            current_viewer_id: None,
            hostname,
            is_elevated,
            start_time: Instant::now(),
        }
    }

    /// Connect to the server
    pub async fn connect(&mut self) -> Result<()> {
        self.state = SessionState::Connecting;

        let transport = WebSocketTransport::connect(
            &self.config.server_url,
            &self.config.agent_id,
            &self.config.api_key,
            Some(&self.hostname),
            self.config.support_code.as_deref(),
        ).await?;

        self.transport = Some(transport);
        self.state = SessionState::Idle;  // Start in idle mode

        tracing::info!("Connected to server, entering idle mode");

        Ok(())
    }

    /// Initialize streaming resources (capturer, encoder, input)
    fn init_streaming(&mut self) -> Result<()> {
        if self.capturer.is_some() {
            return Ok(()); // Already initialized
        }

        tracing::info!("Initializing streaming resources...");

        // Get primary display
        let primary_display = capture::primary_display()?;
        tracing::info!("Using display: {} ({}x{})",
            primary_display.name, primary_display.width, primary_display.height);

        // Create capturer
        let capturer = capture::create_capturer(
            primary_display.clone(),
            self.config.capture.use_dxgi,
            self.config.capture.gdi_fallback,
        )?;
        self.capturer = Some(capturer);

        // Create encoder
        let encoder = encoder::create_encoder(
            &self.config.encoding.codec,
            self.config.encoding.quality,
        )?;
        self.encoder = Some(encoder);

        // Create input controller
        let input = InputController::new()?;
        self.input = Some(input);

        tracing::info!("Streaming resources initialized");
        Ok(())
    }

    /// Release streaming resources to save CPU/memory when idle
    fn release_streaming(&mut self) {
        if self.capturer.is_some() {
            tracing::info!("Releasing streaming resources");
            self.capturer = None;
            self.encoder = None;
            self.input = None;
            self.current_viewer_id = None;
        }
    }

    /// Get display count for status reports
    fn get_display_count(&self) -> i32 {
        capture::enumerate_displays().map(|d| d.len() as i32).unwrap_or(1)
    }

    /// Send agent status to server
    async fn send_status(&mut self) -> Result<()> {
        let status = AgentStatus {
            hostname: self.hostname.clone(),
            os_version: std::env::consts::OS.to_string(),
            is_elevated: self.is_elevated,
            uptime_secs: self.start_time.elapsed().as_secs() as i64,
            display_count: self.get_display_count(),
            is_streaming: self.state == SessionState::Streaming,
        };

        let msg = Message {
            payload: Some(message::Payload::AgentStatus(status)),
        };

        if let Some(transport) = self.transport.as_mut() {
            transport.send(msg).await?;
        }

        Ok(())
    }

    /// Send heartbeat to server
    async fn send_heartbeat(&mut self) -> Result<()> {
        let heartbeat = Heartbeat {
            timestamp: chrono::Utc::now().timestamp_millis(),
        };

        let msg = Message {
            payload: Some(message::Payload::Heartbeat(heartbeat)),
        };

        if let Some(transport) = self.transport.as_mut() {
            transport.send(msg).await?;
        }

        Ok(())
    }

    /// Run the session main loop with tray and chat event processing
    pub async fn run_with_tray(&mut self, tray: Option<&TrayController>, chat: Option<&ChatController>) -> Result<()> {
        if self.transport.is_none() {
            anyhow::bail!("Not connected");
        }

        // Send initial status
        self.send_status().await?;

        // Timing for heartbeat and status
        let mut last_heartbeat = Instant::now();
        let mut last_status = Instant::now();
        let mut last_frame_time = Instant::now();
        let frame_interval = Duration::from_millis(1000 / self.config.capture.fps as u64);

        // Main loop
        loop {
            // Process tray events
            if let Some(t) = tray {
                if let Some(action) = t.process_events() {
                    match action {
                        TrayAction::EndSession => {
                            tracing::info!("User requested session end via tray");
                            return Err(anyhow::anyhow!("USER_EXIT: Session ended by user"));
                        }
                        TrayAction::ShowDetails => {
                            tracing::info!("User requested details (not yet implemented)");
                        }
                        TrayAction::ShowDebugWindow => {
                            show_debug_console();
                        }
                    }
                }

                if t.exit_requested() {
                    tracing::info!("Exit requested via tray");
                    return Err(anyhow::anyhow!("USER_EXIT: Exit requested by user"));
                }
            }

            // Process incoming messages
            let messages: Vec<Message> = {
                let transport = self.transport.as_mut().unwrap();
                let mut msgs = Vec::new();
                while let Some(msg) = transport.try_recv()? {
                    msgs.push(msg);
                }
                msgs
            };

            for msg in messages {
                // Handle chat messages specially
                if let Some(message::Payload::ChatMessage(chat_msg)) = &msg.payload {
                    if let Some(c) = chat {
                        c.add_message(ChatMsg {
                            id: chat_msg.id.clone(),
                            sender: chat_msg.sender.clone(),
                            content: chat_msg.content.clone(),
                            timestamp: chat_msg.timestamp,
                        });
                    }
                    continue;
                }

                // Handle control messages that affect state
                if let Some(ref payload) = msg.payload {
                    match payload {
                        message::Payload::StartStream(start) => {
                            tracing::info!("StartStream received from viewer: {}", start.viewer_id);
                            if let Err(e) = self.init_streaming() {
                                tracing::error!("Failed to init streaming: {}", e);
                            } else {
                                self.state = SessionState::Streaming;
                                self.current_viewer_id = Some(start.viewer_id.clone());
                                tracing::info!("Now streaming to viewer {}", start.viewer_id);
                            }
                            continue;
                        }
                        message::Payload::StopStream(stop) => {
                            tracing::info!("StopStream received for viewer: {}", stop.viewer_id);
                            // Only stop if it matches current viewer
                            if self.current_viewer_id.as_ref() == Some(&stop.viewer_id) {
                                self.release_streaming();
                                self.state = SessionState::Idle;
                                tracing::info!("Stopped streaming, returning to idle mode");
                            }
                            continue;
                        }
                        message::Payload::Heartbeat(hb) => {
                            // Respond to server heartbeat with ack
                            let ack = HeartbeatAck {
                                client_timestamp: hb.timestamp,
                                server_timestamp: chrono::Utc::now().timestamp_millis(),
                            };
                            let ack_msg = Message {
                                payload: Some(message::Payload::HeartbeatAck(ack)),
                            };
                            if let Some(transport) = self.transport.as_mut() {
                                let _ = transport.send(ack_msg).await;
                            }
                            continue;
                        }
                        _ => {}
                    }
                }

                // Handle other messages (input events, disconnect, etc.)
                self.handle_message(msg)?;
            }

            // Check for outgoing chat messages
            if let Some(c) = chat {
                if let Some(outgoing) = c.poll_outgoing() {
                    let chat_proto = ChatMessage {
                        id: outgoing.id,
                        sender: "client".to_string(),
                        content: outgoing.content,
                        timestamp: outgoing.timestamp,
                    };
                    let msg = Message {
                        payload: Some(message::Payload::ChatMessage(chat_proto)),
                    };
                    let transport = self.transport.as_mut().unwrap();
                    transport.send(msg).await?;
                }
            }

            // State-specific behavior
            match self.state {
                SessionState::Idle => {
                    // In idle mode, just send heartbeats and status periodically
                    if last_heartbeat.elapsed() >= HEARTBEAT_INTERVAL {
                        last_heartbeat = Instant::now();
                        if let Err(e) = self.send_heartbeat().await {
                            tracing::warn!("Failed to send heartbeat: {}", e);
                        }
                    }

                    if last_status.elapsed() >= STATUS_INTERVAL {
                        last_status = Instant::now();
                        if let Err(e) = self.send_status().await {
                            tracing::warn!("Failed to send status: {}", e);
                        }
                    }

                    // Longer sleep in idle mode to reduce CPU usage
                    tokio::time::sleep(Duration::from_millis(100)).await;
                }
                SessionState::Streaming => {
                    // In streaming mode, capture and send frames
                    if last_frame_time.elapsed() >= frame_interval {
                        last_frame_time = Instant::now();

                        if let (Some(capturer), Some(encoder)) =
                            (self.capturer.as_mut(), self.encoder.as_mut())
                        {
                            if let Ok(Some(frame)) = capturer.capture() {
                                if let Ok(encoded) = encoder.encode(&frame) {
                                    if encoded.size > 0 {
                                        let msg = Message {
                                            payload: Some(message::Payload::VideoFrame(encoded.frame)),
                                        };
                                        let transport = self.transport.as_mut().unwrap();
                                        if let Err(e) = transport.send(msg).await {
                                            tracing::warn!("Failed to send frame: {}", e);
                                        }
                                    }
                                }
                            }
                        }
                    }

                    // Short sleep in streaming mode
                    tokio::time::sleep(Duration::from_millis(1)).await;
                }
                _ => {
                    // Disconnected or connecting - shouldn't be in main loop
                    tokio::time::sleep(Duration::from_millis(100)).await;
                }
            }

            // Check if still connected
            if let Some(transport) = self.transport.as_ref() {
                if !transport.is_connected() {
                    tracing::warn!("Connection lost");
                    break;
                }
            } else {
                tracing::warn!("Transport is None");
                break;
            }
        }

        self.release_streaming();
        self.state = SessionState::Disconnected;
        Ok(())
    }

    /// Handle incoming message from server
    fn handle_message(&mut self, msg: Message) -> Result<()> {
        match msg.payload {
            Some(message::Payload::MouseEvent(mouse)) => {
                if let Some(input) = self.input.as_mut() {
                    use crate::proto::MouseEventType;
                    use crate::input::MouseButton;

                    match MouseEventType::try_from(mouse.event_type).unwrap_or(MouseEventType::MouseMove) {
                        MouseEventType::MouseMove => {
                            input.mouse_move(mouse.x, mouse.y)?;
                        }
                        MouseEventType::MouseDown => {
                            input.mouse_move(mouse.x, mouse.y)?;
                            if let Some(ref buttons) = mouse.buttons {
                                if buttons.left { input.mouse_click(MouseButton::Left, true)?; }
                                if buttons.right { input.mouse_click(MouseButton::Right, true)?; }
                                if buttons.middle { input.mouse_click(MouseButton::Middle, true)?; }
                            }
                        }
                        MouseEventType::MouseUp => {
                            if let Some(ref buttons) = mouse.buttons {
                                if buttons.left { input.mouse_click(MouseButton::Left, false)?; }
                                if buttons.right { input.mouse_click(MouseButton::Right, false)?; }
                                if buttons.middle { input.mouse_click(MouseButton::Middle, false)?; }
                            }
                        }
                        MouseEventType::MouseWheel => {
                            input.mouse_scroll(mouse.wheel_delta_x, mouse.wheel_delta_y)?;
                        }
                    }
                }
            }

            Some(message::Payload::KeyEvent(key)) => {
                if let Some(input) = self.input.as_mut() {
                    input.key_event(key.vk_code as u16, key.down)?;
                }
            }

            Some(message::Payload::SpecialKey(special)) => {
                if let Some(input) = self.input.as_mut() {
                    use crate::proto::SpecialKey;
                    match SpecialKey::try_from(special.key).ok() {
                        Some(SpecialKey::CtrlAltDel) => {
                            input.send_ctrl_alt_del()?;
                        }
                        _ => {}
                    }
                }
            }

            Some(message::Payload::AdminCommand(cmd)) => {
                use crate::proto::AdminCommandType;
                tracing::info!("Admin command received: {:?} - {}", cmd.command, cmd.reason);

                match AdminCommandType::try_from(cmd.command).ok() {
                    Some(AdminCommandType::AdminUninstall) => {
                        tracing::warn!("Uninstall command received from server");
                        // Return special error to trigger uninstall in main loop
                        return Err(anyhow::anyhow!("ADMIN_UNINSTALL: {}", cmd.reason));
                    }
                    Some(AdminCommandType::AdminRestart) => {
                        tracing::info!("Restart command received from server");
                        // For now, just disconnect - the auto-restart logic will handle it
                        return Err(anyhow::anyhow!("ADMIN_RESTART: {}", cmd.reason));
                    }
                    Some(AdminCommandType::AdminUpdate) => {
                        tracing::info!("Update command received (not implemented)");
                    }
                    None => {
                        tracing::warn!("Unknown admin command: {}", cmd.command);
                    }
                }
            }

            Some(message::Payload::Disconnect(disc)) => {
                tracing::info!("Disconnect requested: {}", disc.reason);
                if disc.reason.contains("cancelled") {
                    return Err(anyhow::anyhow!("SESSION_CANCELLED: {}", disc.reason));
                }
                if disc.reason.contains("administrator") || disc.reason.contains("Disconnected") {
                    return Err(anyhow::anyhow!("ADMIN_DISCONNECT: {}", disc.reason));
                }
                return Err(anyhow::anyhow!("Disconnect: {}", disc.reason));
            }

            _ => {
                // Ignore unknown messages
            }
        }

        Ok(())
    }
}


Excellent! Now let me check the proto file to understand the message structure and look for any existing admin commands or update infrastructure:


     1→syntax = "proto3";
     2→package guruconnect;
     3→
     4→// ============================================================================
     5→// Session Management
     6→// ============================================================================
     7→
     8→message SessionRequest {
     9→    string agent_id = 1;
    10→    string session_token = 2;
    11→    SessionType session_type = 3;
    12→    string client_version = 4;
    13→}
    14→
    15→message SessionResponse {
    16→    bool success = 1;
    17→    string session_id = 2;
    18→    string error = 3;
    19→    DisplayInfo display_info = 4;
    20→}
    21→
    22→enum SessionType {
    23→    SCREEN_CONTROL = 0;
    24→    VIEW_ONLY = 1;
    25→    BACKSTAGE = 2;
    26→    FILE_TRANSFER = 3;
    27→}
    28→
    29→// ============================================================================
    30→// Display Information
    31→// ============================================================================
    32→
    33→message DisplayInfo {
    34→    repeated Display displays = 1;
    35→    int32 primary_display = 2;
    36→}
    37→
    38→message Display {
    39→    int32 id = 1;
    40→    string name = 2;
    41→    int32 x = 3;
    42→    int32 y = 4;
    43→    int32 width = 5;
    44→    int32 height = 6;
    45→    bool is_primary = 7;
    46→}
    47→
    48→message SwitchDisplay {
    49→    int32 display_id = 1;
    50→}
    51→
    52→// ============================================================================
    53→// Video Frames
    54→// ============================================================================
    55→
    56→message VideoFrame {
    57→    int64 timestamp = 1;
    58→    int32 display_id = 2;
    59→    int32 sequence = 3;
    60→
    61→    oneof encoding {
    62→        RawFrame raw = 10;
    63→        EncodedFrame vp9 = 11;
    64→        EncodedFrame h264 = 12;
    65→        EncodedFrame h265 = 13;
    66→    }
    67→}
    68→
    69→message RawFrame {
    70→    int32 width = 1;
    71→    int32 height = 2;
    72→    bytes data = 3;             // Zstd compressed BGRA
    73→    bool compressed = 4;
    74→    repeated DirtyRect dirty_rects = 5;
    75→    bool is_keyframe = 6;       // Full frame vs incremental
    76→}
    77→
    78→message DirtyRect {
    79→    int32 x = 1;
    80→    int32 y = 2;
    81→    int32 width = 3;
    82→    int32 height = 4;
    83→}
    84→
    85→message EncodedFrame {
    86→    bytes data = 1;
    87→    bool keyframe = 2;
    88→    int64 pts = 3;
    89→    int64 dts = 4;
    90→}
    91→
    92→message VideoAck {
    93→    int32 sequence = 1;
    94→    int64 timestamp = 2;
    95→}
    96→
    97→// ============================================================================
    98→// Cursor
    99→// ============================================================================
   100→
   101→message CursorShape {
   102→    uint64 id = 1;
   103→    int32 hotspot_x = 2;
   104→    int32 hotspot_y = 3;
   105→    int32 width = 4;
   106→    int32 height = 5;
   107→    bytes data = 6;             // BGRA bitmap
   108→}
   109→
   110→message CursorPosition {
   111→    int32 x = 1;
   112→    int32 y = 2;
   113→    bool visible = 3;
   114→}
   115→
   116→// ============================================================================
   117→// Input Events
   118→// ============================================================================
   119→
   120→message MouseEvent {
   121→    int32 x = 1;
   122→    int32 y = 2;
   123→    MouseButtons buttons = 3;
   124→    int32 wheel_delta_x = 4;
   125→    int32 wheel_delta_y = 5;
   126→    MouseEventType event_type = 6;
   127→}
   128→
   129→enum MouseEventType {
   130→    MOUSE_MOVE = 0;
   131→    MOUSE_DOWN = 1;
   132→    MOUSE_UP = 2;
   133→    MOUSE_WHEEL = 3;
   134→}
   135→
   136→message MouseButtons {
   137→    bool left = 1;
   138→    bool right = 2;
   139→    bool middle = 3;
   140→    bool x1 = 4;
   141→    bool x2 = 5;
   142→}
   143→
   144→message KeyEvent {
   145→    bool down = 1;              // true = key down, false = key up
   146→    KeyEventType key_type = 2;
   147→    uint32 vk_code = 3;         // Virtual key code (Windows VK_*)
   148→    uint32 scan_code = 4;       // Hardware scan code
   149→    string unicode = 5;         // Unicode character (for text input)
   150→    Modifiers modifiers = 6;
   151→}
   152→
   153→enum KeyEventType {
   154→    KEY_VK = 0;                 // Virtual key code
   155→    KEY_SCAN = 1;               // Scan code
   156→    KEY_UNICODE = 2;            // Unicode character
   157→}
   158→
   159→message Modifiers {
   160→    bool ctrl = 1;
   161→    bool alt = 2;
   162→    bool shift = 3;
   163→    bool meta = 4;              // Windows key
   164→    bool caps_lock = 5;
   165→    bool num_lock = 6;
   166→}
   167→
   168→message SpecialKeyEvent {
   169→    SpecialKey key = 1;
   170→}
   171→
   172→enum SpecialKey {
   173→    CTRL_ALT_DEL = 0;
   174→    LOCK_SCREEN = 1;
   175→    PRINT_SCREEN = 2;
   176→}
   177→
   178→// ============================================================================
   179→// Clipboard
   180→// ============================================================================
   181→
   182→message ClipboardData {
   183→    ClipboardFormat format = 1;
   184→    bytes data = 2;
   185→    string mime_type = 3;
   186→}
   187→
   188→enum ClipboardFormat {
   189→    CLIPBOARD_TEXT = 0;
   190→    CLIPBOARD_HTML = 1;
   191→    CLIPBOARD_RTF = 2;
   192→    CLIPBOARD_IMAGE = 3;
   193→    CLIPBOARD_FILES = 4;
   194→}
   195→
   196→message ClipboardRequest {
   197→    // Request current clipboard content
   198→}
   199→
   200→// ============================================================================
   201→// Quality Control
   202→// ============================================================================
   203→
   204→message QualitySettings {
   205→    QualityPreset preset = 1;
   206→    int32 custom_fps = 2;       // 1-60
   207→    int32 custom_bitrate = 3;   // kbps
   208→    CodecPreference codec = 4;
   209→}
   210→
   211→enum QualityPreset {
   212→    QUALITY_AUTO = 0;
   213→    QUALITY_LOW = 1;            // Low bandwidth
   214→    QUALITY_BALANCED = 2;
   215→    QUALITY_HIGH = 3;           // Best quality
   216→}
   217→
   218→enum CodecPreference {
   219→    CODEC_AUTO = 0;
   220→    CODEC_RAW = 1;              // Raw + Zstd (LAN)
   221→    CODEC_VP9 = 2;
   222→    CODEC_H264 = 3;
   223→    CODEC_H265 = 4;
   224→}
   225→
   226→message LatencyReport {
   227→    int64 rtt_ms = 1;
   228→    int32 fps = 2;
   229→    int32 bitrate_kbps = 3;
   230→}
   231→
   232→// ============================================================================
   233→// Chat Messages
   234→// ============================================================================
   235→
   236→message ChatMessage {
   237→    string id = 1;              // Unique message ID
   238→    string sender = 2;          // "technician" or "client"
   239→    string content = 3;         // Message text
   240→    int64 timestamp = 4;        // Unix timestamp
   241→}
   242→
   243→// ============================================================================
   244→// Control Messages
   245→// ============================================================================
   246→
   247→message Heartbeat {
   248→    int64 timestamp = 1;
   249→}
   250→
   251→message HeartbeatAck {
   252→    int64 client_timestamp = 1;
   253→    int64 server_timestamp = 2;
   254→}
   255→
   256→message Disconnect {
   257→    string reason = 1;
   258→}
   259→
   260→// Server commands agent to start streaming video
   261→message StartStream {
   262→    string viewer_id = 1;           // ID of viewer requesting stream
   263→    int32 display_id = 2;           // Which display to stream (0 = primary)
   264→}
   265→
   266→// Server commands agent to stop streaming
   267→message StopStream {
   268→    string viewer_id = 1;           // Which viewer disconnected
   269→}
   270→
   271→// Agent reports its status periodically when idle
   272→message AgentStatus {
   273→    string hostname = 1;
   274→    string os_version = 2;
   275→    bool is_elevated = 3;
   276→    int64 uptime_secs = 4;
   277→    int32 display_count = 5;
   278→    bool is_streaming = 6;
   279→}
   280→
   281→// Server commands agent to uninstall itself
   282→message AdminCommand {
   283→    AdminCommandType command = 1;
   284→    string reason = 2;              // Why the command was issued
   285→}
   286→
   287→enum AdminCommandType {
   288→    ADMIN_UNINSTALL = 0;            // Uninstall agent and remove from startup
   289→    ADMIN_RESTART = 1;              // Restart the agent process
   290→    ADMIN_UPDATE = 2;               // Download and install update (future)
   291→}
   292→
   293→// ============================================================================
   294→// Top-Level Message Wrapper
   295→// ============================================================================
   296→
   297→message Message {
   298→    oneof payload {
   299→        // Session
   300→        SessionRequest session_request = 1;
   301→        SessionResponse session_response = 2;
   302→
   303→        // Video
   304→        VideoFrame video_frame = 10;
   305→        VideoAck video_ack = 11;
   306→        SwitchDisplay switch_display = 12;
   307→
   308→        // Cursor
   309→        CursorShape cursor_shape = 15;
   310→        CursorPosition cursor_position = 16;
   311→
   312→        // Input
   313→        MouseEvent mouse_event = 20;
   314→        KeyEvent key_event = 21;
   315→        SpecialKeyEvent special_key = 22;
   316→
   317→        // Clipboard
   318→        ClipboardData clipboard_data = 30;
   319→        ClipboardRequest clipboard_request = 31;
   320→
   321→        // Quality
   322→        QualitySettings quality_settings = 40;
   323→        LatencyReport latency_report = 41;
   324→
   325→        // Control
   326→        Heartbeat heartbeat = 50;
   327→        HeartbeatAck heartbeat_ack = 51;
   328→        Disconnect disconnect = 52;
   329→        StartStream start_stream = 53;
   330→        StopStream stop_stream = 54;
   331→        AgentStatus agent_status = 55;
   332→
   333→        // Chat
   334→        ChatMessage chat_message = 60;
   335→
   336→        // Admin commands (server -> agent)
   337→        AdminCommand admin_command = 70;
   338→    }
   339→}
   340→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

syntax = "proto3";
package guruconnect;

// ============================================================================
// Session Management
// ============================================================================

message SessionRequest {
    string agent_id = 1;
    string session_token = 2;
    SessionType session_type = 3;
    string client_version = 4;
}

message SessionResponse {
    bool success = 1;
    string session_id = 2;
    string error = 3;
    DisplayInfo display_info = 4;
}

enum SessionType {
    SCREEN_CONTROL = 0;
    VIEW_ONLY = 1;
    BACKSTAGE = 2;
    FILE_TRANSFER = 3;
}

// ============================================================================
// Display Information
// ============================================================================

message DisplayInfo {
    repeated Display displays = 1;
    int32 primary_display = 2;
}

message Display {
    int32 id = 1;
    string name = 2;
    int32 x = 3;
    int32 y = 4;
    int32 width = 5;
    int32 height = 6;
    bool is_primary = 7;
}

message SwitchDisplay {
    int32 display_id = 1;
}

// ============================================================================
// Video Frames
// ============================================================================

message VideoFrame {
    int64 timestamp = 1;
    int32 display_id = 2;
    int32 sequence = 3;

    oneof encoding {
        RawFrame raw = 10;
        EncodedFrame vp9 = 11;
        EncodedFrame h264 = 12;
        EncodedFrame h265 = 13;
    }
}

message RawFrame {
    int32 width = 1;
    int32 height = 2;
    bytes data = 3;             // Zstd compressed BGRA
    bool compressed = 4;
    repeated DirtyRect dirty_rects = 5;
    bool is_keyframe = 6;       // Full frame vs incremental
}

message DirtyRect {
    int32 x = 1;
    int32 y = 2;
    int32 width = 3;
    int32 height = 4;
}

message EncodedFrame {
    bytes data = 1;
    bool keyframe = 2;
    int64 pts = 3;
    int64 dts = 4;
}

message VideoAck {
    int32 sequence = 1;
    int64 timestamp = 2;
}

// ============================================================================
// Cursor
// ============================================================================

message CursorShape {
    uint64 id = 1;
    int32 hotspot_x = 2;
    int32 hotspot_y = 3;
    int32 width = 4;
    int32 height = 5;
    bytes data = 6;             // BGRA bitmap
}

message CursorPosition {
    int32 x = 1;
    int32 y = 2;
    bool visible = 3;
}

// ============================================================================
// Input Events
// ============================================================================

message MouseEvent {
    int32 x = 1;
    int32 y = 2;
    MouseButtons buttons = 3;
    int32 wheel_delta_x = 4;
    int32 wheel_delta_y = 5;
    MouseEventType event_type = 6;
}

enum MouseEventType {
    MOUSE_MOVE = 0;
    MOUSE_DOWN = 1;
    MOUSE_UP = 2;
    MOUSE_WHEEL = 3;
}

message MouseButtons {
    bool left = 1;
    bool right = 2;
    bool middle = 3;
    bool x1 = 4;
    bool x2 = 5;
}

message KeyEvent {
    bool down = 1;              // true = key down, false = key up
    KeyEventType key_type = 2;
    uint32 vk_code = 3;         // Virtual key code (Windows VK_*)
    uint32 scan_code = 4;       // Hardware scan code
    string unicode = 5;         // Unicode character (for text input)
    Modifiers modifiers = 6;
}

enum KeyEventType {
    KEY_VK = 0;                 // Virtual key code
    KEY_SCAN = 1;               // Scan code
    KEY_UNICODE = 2;            // Unicode character
}

message Modifiers {
    bool ctrl = 1;
    bool alt = 2;
    bool shift = 3;
    bool meta = 4;              // Windows key
    bool caps_lock = 5;
    bool num_lock = 6;
}

message SpecialKeyEvent {
    SpecialKey key = 1;
}

enum SpecialKey {
    CTRL_ALT_DEL = 0;
    LOCK_SCREEN = 1;
    PRINT_SCREEN = 2;
}

// ============================================================================
// Clipboard
// ============================================================================

message ClipboardData {
    ClipboardFormat format = 1;
    bytes data = 2;
    string mime_type = 3;
}

enum ClipboardFormat {
    CLIPBOARD_TEXT = 0;
    CLIPBOARD_HTML = 1;
    CLIPBOARD_RTF = 2;
    CLIPBOARD_IMAGE = 3;
    CLIPBOARD_FILES = 4;
}

message ClipboardRequest {
    // Request current clipboard content
}

// ============================================================================
// Quality Control
// ============================================================================

message QualitySettings {
    QualityPreset preset = 1;
    int32 custom_fps = 2;       // 1-60
    int32 custom_bitrate = 3;   // kbps
    CodecPreference codec = 4;
}

enum QualityPreset {
    QUALITY_AUTO = 0;
    QUALITY_LOW = 1;            // Low bandwidth
    QUALITY_BALANCED = 2;
    QUALITY_HIGH = 3;           // Best quality
}

enum CodecPreference {
    CODEC_AUTO = 0;
    CODEC_RAW = 1;              // Raw + Zstd (LAN)
    CODEC_VP9 = 2;
    CODEC_H264 = 3;
    CODEC_H265 = 4;
}

message LatencyReport {
    int64 rtt_ms = 1;
    int32 fps = 2;
    int32 bitrate_kbps = 3;
}

// ============================================================================
// Chat Messages
// ============================================================================

message ChatMessage {
    string id = 1;              // Unique message ID
    string sender = 2;          // "technician" or "client"
    string content = 3;         // Message text
    int64 timestamp = 4;        // Unix timestamp
}

// ============================================================================
// Control Messages
// ============================================================================

message Heartbeat {
    int64 timestamp = 1;
}

message HeartbeatAck {
    int64 client_timestamp = 1;
    int64 server_timestamp = 2;
}

message Disconnect {
    string reason = 1;
}

// Server commands agent to start streaming video
message StartStream {
    string viewer_id = 1;           // ID of viewer requesting stream
    int32 display_id = 2;           // Which display to stream (0 = primary)
}

// Server commands agent to stop streaming
message StopStream {
    string viewer_id = 1;           // Which viewer disconnected
}

// Agent reports its status periodically when idle
message AgentStatus {
    string hostname = 1;
    string os_version = 2;
    bool is_elevated = 3;
    int64 uptime_secs = 4;
    int32 display_count = 5;
    bool is_streaming = 6;
}

// Server commands agent to uninstall itself
message AdminCommand {
    AdminCommandType command = 1;
    string reason = 2;              // Why the command was issued
}

enum AdminCommandType {
    ADMIN_UNINSTALL = 0;            // Uninstall agent and remove from startup
    ADMIN_RESTART = 1;              // Restart the agent process
    ADMIN_UPDATE = 2;               // Download and install update (future)
}

// ============================================================================
// Top-Level Message Wrapper
// ============================================================================

message Message {
    oneof payload {
        // Session
        SessionRequest session_request = 1;
        SessionResponse session_response = 2;

        // Video
        VideoFrame video_frame = 10;
        VideoAck video_ack = 11;
        SwitchDisplay switch_display = 12;

        // Cursor
        CursorShape cursor_shape = 15;
        CursorPosition cursor_position = 16;

        // Input
        MouseEvent mouse_event = 20;
        KeyEvent key_event = 21;
        SpecialKeyEvent special_key = 22;

        // Clipboard
        ClipboardData clipboard_data = 30;
        ClipboardRequest clipboard_request = 31;

        // Quality
        QualitySettings quality_settings = 40;
        LatencyReport latency_report = 41;

        // Control
        Heartbeat heartbeat = 50;
        HeartbeatAck heartbeat_ack = 51;
        Disconnect disconnect = 52;
        StartStream start_stream = 53;
        StopStream stop_stream = 54;
        AgentStatus agent_status = 55;

        // Chat
        ChatMessage chat_message = 60;

        // Admin commands (server -> agent)
        AdminCommand admin_command = 70;
    }
}


Perfect! Now let me check the build.rs to see how version info is currently embedded: