Add chat functionality between technician and client
- Add ChatMessage to protobuf definitions - Server relays chat messages between agent and viewer - Agent chat module shows messages via MessageBox - Dashboard chat modal with WebSocket connection - Simplified protobuf encoder/decoder in JavaScript 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
172
agent/src/chat/mod.rs
Normal file
172
agent/src/chat/mod.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
//! Chat window for the agent
|
||||
//!
|
||||
//! Provides a simple chat interface for communication between
|
||||
//! the technician and the end user.
|
||||
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use tracing::{info, warn, error};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::UI::WindowsAndMessaging::*;
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::Foundation::*;
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::Graphics::Gdi::*;
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
#[cfg(windows)]
|
||||
use windows::core::PCWSTR;
|
||||
|
||||
/// A chat message
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChatMessage {
|
||||
pub id: String,
|
||||
pub sender: String,
|
||||
pub content: String,
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
/// Commands that can be sent to the chat window
|
||||
#[derive(Debug)]
|
||||
pub enum ChatCommand {
|
||||
Show,
|
||||
Hide,
|
||||
AddMessage(ChatMessage),
|
||||
Close,
|
||||
}
|
||||
|
||||
/// Controller for the chat window
|
||||
pub struct ChatController {
|
||||
command_tx: Sender<ChatCommand>,
|
||||
message_rx: Arc<Mutex<Receiver<ChatMessage>>>,
|
||||
_handle: thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl ChatController {
|
||||
/// Create a new chat controller (spawns chat window thread)
|
||||
#[cfg(windows)]
|
||||
pub fn new() -> Option<Self> {
|
||||
let (command_tx, command_rx) = mpsc::channel::<ChatCommand>();
|
||||
let (message_tx, message_rx) = mpsc::channel::<ChatMessage>();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
run_chat_window(command_rx, message_tx);
|
||||
});
|
||||
|
||||
Some(Self {
|
||||
command_tx,
|
||||
message_rx: Arc::new(Mutex::new(message_rx)),
|
||||
_handle: handle,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn new() -> Option<Self> {
|
||||
warn!("Chat window not supported on this platform");
|
||||
None
|
||||
}
|
||||
|
||||
/// Show the chat window
|
||||
pub fn show(&self) {
|
||||
let _ = self.command_tx.send(ChatCommand::Show);
|
||||
}
|
||||
|
||||
/// Hide the chat window
|
||||
pub fn hide(&self) {
|
||||
let _ = self.command_tx.send(ChatCommand::Hide);
|
||||
}
|
||||
|
||||
/// Add a message to the chat window
|
||||
pub fn add_message(&self, msg: ChatMessage) {
|
||||
let _ = self.command_tx.send(ChatCommand::AddMessage(msg));
|
||||
}
|
||||
|
||||
/// Check for outgoing messages from the user
|
||||
pub fn poll_outgoing(&self) -> Option<ChatMessage> {
|
||||
if let Ok(rx) = self.message_rx.lock() {
|
||||
rx.try_recv().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the chat window
|
||||
pub fn close(&self) {
|
||||
let _ = self.command_tx.send(ChatCommand::Close);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn run_chat_window(command_rx: Receiver<ChatCommand>, message_tx: Sender<ChatMessage>) {
|
||||
use std::ffi::OsStr;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
info!("Starting chat window thread");
|
||||
|
||||
// For now, we'll use a simple message box approach
|
||||
// A full implementation would create a proper window with a text input
|
||||
|
||||
// Process commands
|
||||
loop {
|
||||
match command_rx.recv() {
|
||||
Ok(ChatCommand::Show) => {
|
||||
info!("Chat window: Show requested");
|
||||
// Show a simple notification that chat is available
|
||||
}
|
||||
Ok(ChatCommand::Hide) => {
|
||||
info!("Chat window: Hide requested");
|
||||
}
|
||||
Ok(ChatCommand::AddMessage(msg)) => {
|
||||
info!("Chat message received: {} - {}", msg.sender, msg.content);
|
||||
|
||||
// Show the message to the user via a message box (simple implementation)
|
||||
let title = format!("Message from {}", msg.sender);
|
||||
let content = msg.content.clone();
|
||||
|
||||
// Spawn a thread to show the message box (non-blocking)
|
||||
thread::spawn(move || {
|
||||
show_message_box_internal(&title, &content);
|
||||
});
|
||||
}
|
||||
Ok(ChatCommand::Close) => {
|
||||
info!("Chat window: Close requested");
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
// Channel closed
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn show_message_box_internal(title: &str, message: &str) {
|
||||
use std::ffi::OsStr;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
let title_wide: Vec<u16> = OsStr::new(title)
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
let message_wide: Vec<u16> = OsStr::new(message)
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
|
||||
unsafe {
|
||||
MessageBoxW(
|
||||
None,
|
||||
PCWSTR(message_wide.as_ptr()),
|
||||
PCWSTR(title_wide.as_ptr()),
|
||||
MB_OK | MB_ICONINFORMATION | MB_TOPMOST | MB_SETFOREGROUND,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn run_chat_window(_command_rx: Receiver<ChatCommand>, _message_tx: Sender<ChatMessage>) {
|
||||
// No-op on non-Windows
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
//! for a one-time support session.
|
||||
|
||||
mod capture;
|
||||
mod chat;
|
||||
mod config;
|
||||
mod encoder;
|
||||
mod input;
|
||||
@@ -198,6 +199,12 @@ async fn run_agent(config: config::Config) -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
// Create chat controller
|
||||
let chat_ctrl = chat::ChatController::new();
|
||||
if chat_ctrl.is_some() {
|
||||
info!("Chat controller created");
|
||||
}
|
||||
|
||||
// Connect to server and run main loop
|
||||
loop {
|
||||
info!("Connecting to server...");
|
||||
@@ -219,8 +226,8 @@ async fn run_agent(config: config::Config) -> Result<()> {
|
||||
t.update_status("Status: Connected");
|
||||
}
|
||||
|
||||
// Run session until disconnect, passing tray for event processing
|
||||
if let Err(e) = session.run_with_tray(tray.as_ref()).await {
|
||||
// Run session until disconnect, passing tray and chat for event processing
|
||||
if let Err(e) = session.run_with_tray(tray.as_ref(), chat_ctrl.as_ref()).await {
|
||||
let error_msg = e.to_string();
|
||||
|
||||
// Check if this is a user-initiated exit
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
//! - Input event handling
|
||||
|
||||
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;
|
||||
use crate::proto::{Message, message};
|
||||
use crate::proto::{Message, message, ChatMessage};
|
||||
use crate::transport::WebSocketTransport;
|
||||
use crate::tray::{TrayController, TrayAction};
|
||||
use anyhow::Result;
|
||||
@@ -148,8 +149,8 @@ impl SessionManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run the session main loop with tray event processing
|
||||
pub async fn run_with_tray(&mut self, tray: Option<&TrayController>) -> Result<()> {
|
||||
/// 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");
|
||||
}
|
||||
@@ -215,9 +216,38 @@ impl SessionManager {
|
||||
};
|
||||
|
||||
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; // Don't pass to handle_message
|
||||
}
|
||||
self.handle_message(&mut input, msg)?;
|
||||
}
|
||||
|
||||
// Check for outgoing chat messages from user
|
||||
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?;
|
||||
}
|
||||
}
|
||||
|
||||
// Capture and send frame if interval elapsed
|
||||
if last_frame_time.elapsed() >= frame_interval {
|
||||
last_frame_time = Instant::now();
|
||||
|
||||
Reference in New Issue
Block a user