feat(tui): add usage overlay and sidebar for displaying usage statistics and tasks

feat(tui): implement status bar with connection and turn state indicators
feat(tui): create workflow panel for agent status and progress visualization
feat(web): introduce web frontend interface with static file serving
feat(ws): add WebSocket interface for real-time communication and session management
This commit is contained in:
asepharyana
2026-07-20 09:04:57 +07:00
parent bceba665c0
commit da2ed6da25
454 changed files with 13979 additions and 29539 deletions
+36
View File
@@ -0,0 +1,36 @@
//! IPC client — connects to the daemon's Unix socket and sends/receives
//! framed JSON messages.
use std::os::unix::net::UnixStream;
use std::sync::Mutex;
/// A thread-safe IPC client connected to a Zesdex daemon over a Unix socket.
pub struct IpcClient {
conn: Mutex<crate::ipc::conn::Connection>,
}
impl IpcClient {
pub fn connect_unix(path: &str) -> anyhow::Result<Self> {
let stream = UnixStream::connect(path)?;
let conn = crate::ipc::conn::Connection::new(stream);
Ok(Self {
conn: Mutex::new(conn),
})
}
pub fn send<T: serde::Serialize>(&self, msg: &T) -> anyhow::Result<()> {
let mut guard = self
.conn
.lock()
.expect("IpcClient mutex poisoned");
guard.send(msg)
}
pub fn receive<T: serde::de::DeserializeOwned>(&self) -> anyhow::Result<Option<T>> {
let mut guard = self
.conn
.lock()
.expect("IpcClient mutex poisoned");
guard.receive()
}
}
+40
View File
@@ -0,0 +1,40 @@
//! Connection wrapper around a Unix socket stream,
//! pairing a buffered reader with a raw writer.
use std::io::BufReader;
use std::os::unix::net::UnixStream;
/// A framed JSON connection over a Unix socket.
pub struct Connection {
reader: BufReader<UnixStream>,
writer: UnixStream,
}
impl Connection {
pub fn new(stream: UnixStream) -> Self {
let reader = BufReader::new(
stream
.try_clone()
.expect("UnixStream::try_clone should never fail on Linux"),
);
let writer = stream;
Self { reader, writer }
}
pub fn send<T: serde::Serialize>(&mut self, msg: &T) -> anyhow::Result<()> {
let json = serde_json::to_vec(msg)?;
crate::ipc::frame::write_frame(&mut self.writer, &json)?;
Ok(())
}
pub fn receive<T: serde::de::DeserializeOwned>(&mut self) -> anyhow::Result<Option<T>> {
let raw = crate::ipc::frame::read_frame(&mut self.reader)?;
match raw {
None => Ok(None),
Some(bytes) => {
let msg: T = serde_json::from_slice(&bytes)?;
Ok(Some(msg))
}
}
}
}
+51
View File
@@ -0,0 +1,51 @@
//! Length-prefixed framing for Unix-socket IPC.
//!
//! Every message on the wire is encoded as:
//! ```text
//! [ 4-byte big-endian payload length ][ payload bytes (JSON) ]
//! ```
use anyhow::Context;
use std::io::{Read, Write};
const MAX_PAYLOAD: u32 = 64 * 1024 * 1024;
/// Read one length-prefixed frame from `reader`.
pub fn read_frame(reader: &mut impl Read) -> anyhow::Result<Option<Vec<u8>>> {
let mut len_buf = [0u8; 4];
match reader.read_exact(&mut len_buf) {
Ok(()) => {}
Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
return Ok(None);
}
Err(e) => return Err(e).context("failed to read frame length prefix"),
}
let payload_len = u32::from_be_bytes(len_buf) as usize;
if payload_len > MAX_PAYLOAD as usize {
anyhow::bail!("frame payload too large: {payload_len} bytes (max {MAX_PAYLOAD})");
}
let mut payload = vec![0u8; payload_len];
reader.read_exact(&mut payload)?;
Ok(Some(payload))
}
/// Write one length-prefixed frame to `writer`.
pub fn write_frame(writer: &mut impl Write, data: &[u8]) -> anyhow::Result<()> {
let payload_len: u32 = data.len().try_into()?;
if payload_len > MAX_PAYLOAD {
anyhow::bail!("frame payload too large: {payload_len} bytes (max {MAX_PAYLOAD})");
}
let len_bytes = payload_len.to_be_bytes();
writer.write_all(&len_bytes)?;
writer.write_all(data)?;
writer.flush()?;
Ok(())
}
+7
View File
@@ -0,0 +1,7 @@
//! Unix-socket IPC layer for daemon/client communication.
pub mod client;
pub mod conn;
pub mod frame;
pub mod protocol;
pub mod server;
+85
View File
@@ -0,0 +1,85 @@
//! Wire types for the Zesdex IPC protocol.
use serde::{Deserialize, Serialize};
/// A resolved key press sent from the daemon to the client.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum KeyAction {
Char(char),
Enter,
Escape,
Backspace,
Delete,
Tab,
Up,
Down,
Left,
Right,
Home,
End,
PageUp,
PageDown,
Function(u8),
}
/// A message sent from the TUI client to the daemon over the IPC socket.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ClientRequest {
Tick,
KeyPress {
key: KeyAction,
ctrl: bool,
alt: bool,
shift: bool,
},
Submit(String),
Paste(String),
Resize(u16, u16),
Close,
ScrollUp,
ScrollDown,
}
/// A single chat message within a session.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageEntry {
pub role: String,
pub content: String,
pub timestamp: i64,
}
/// A transient toast notification sent to the client.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToastEntry {
pub kind: String,
pub message: String,
pub created_at: i64,
pub lifetime_ms: u64,
}
/// Full UI state snapshot pushed from the daemon to the client.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatePayload {
pub session_id: String,
pub messages: Vec<MessageEntry>,
pub edit_count: u32,
pub message_count: usize,
pub overlay: Option<String>,
pub toasts: Vec<ToastEntry>,
pub dirty: bool,
pub input_buffer: String,
pub input_cursor: usize,
}
/// A frame sent from the daemon to the client.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DaemonFrame {
StateUpdate(Box<StatePayload>),
StreamToken(String),
SystemNote {
kind: String,
message: String,
},
ClipboardCopy(String),
Closed,
}
+26
View File
@@ -0,0 +1,26 @@
//! IPC server — binds a Unix socket and accepts incoming client connections.
use std::os::unix::net::UnixListener;
use std::path::Path;
/// A Unix-socket IPC server.
pub struct IpcServer {
listener: UnixListener,
}
impl IpcServer {
pub fn bind_unix(path: &str) -> anyhow::Result<Self> {
let p = Path::new(path);
if p.exists() {
std::fs::remove_file(p)?;
}
let listener = UnixListener::bind(path)?;
Ok(Self { listener })
}
pub fn accept(&self) -> anyhow::Result<crate::ipc::conn::Connection> {
let (stream, _addr) = self.listener.accept()?;
Ok(crate::ipc::conn::Connection::new(stream))
}
}