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
+164
View File
@@ -0,0 +1,164 @@
//! Zesdex Gateway — main entry point.
//!
//! Assembles domain + application + infrastructure layers and dispatches
//! to the requested interface: TUI (default), daemon (background IPC),
//! API server (REST), WebSocket server, gRPC server, or Web frontend.
//!
//! # CLI flags
//!
//! | Flag | Description |
//! |------|-------------|
//! | `--daemon` | Run as background daemon with IPC socket |
//! | `--attach <id>` | Attach TUI client to a running daemon |
//! | `--api` | Run REST API server |
//! | `--api-port <port>` | REST API port (default 8080) |
//! | `--ws` | Run WebSocket server |
//! | `--ws-port <port>` | WebSocket port (default 8081) |
//! | `--grpc` | Run gRPC server |
//! | `--grpc-port <port>` | gRPC port (default 50051) |
//! | `--web` | Serve web frontend |
//! | `--version` | Print version and exit |
use std::sync::Mutex;
fn main() -> anyhow::Result<()> {
let args: Vec<String> = std::env::args().collect();
let is_daemon = args.iter().any(|a| a == "--daemon");
let is_api = args.iter().any(|a| a == "--api");
let is_ws = args.iter().any(|a| a == "--ws");
let is_grpc = args.iter().any(|a| a == "--grpc");
let is_web = args.iter().any(|a| a == "--web");
let attach_session = args
.iter()
.position(|a| a == "--attach")
.and_then(|i| args.get(i + 1).cloned());
if args.iter().any(|a| a == "--version") {
println!("Zesdex version {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
// ── Setup logging ────────────────────────────────────────────────────
let log_dir = dirs::data_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join("zesdex");
let _ = std::fs::create_dir_all(&log_dir);
let log_path = log_dir.join("zesdex.log");
let log_file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.unwrap_or_else(|_| {
std::fs::OpenOptions::new()
.write(true)
.open("/dev/null")
.expect("cannot open /dev/null")
});
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.with_writer(Mutex::new(log_file))
.init();
tracing::info!("zesdex gateway starting");
// ── Dispatch to interface ────────────────────────────────────────────
// Validate mutually exclusive flags
let mode_count = [is_daemon, is_api, is_ws, is_grpc, is_web]
.iter()
.filter(|&&b| b)
.count()
+ if attach_session.is_some() { 1 } else { 0 };
if mode_count > 1 {
anyhow::bail!(
"Cannot specify multiple modes: --daemon, --attach, --api, --ws, --grpc, --web are mutually exclusive"
);
}
if is_daemon {
tracing::info!("starting in daemon mode");
zesdex_daemon::server::run_daemon()?;
} else if let Some(session_id) = attach_session {
tracing::info!("starting in attach mode for session {session_id}");
zesdex_daemon::client::run_attach(&session_id)?;
} else if is_api {
tracing::info!("starting in API server mode");
run_api_server(&args)?;
} else if is_ws {
tracing::info!("starting in WebSocket server mode");
run_ws_server()?;
} else if is_grpc {
tracing::info!("starting in gRPC server mode");
run_grpc_server()?;
} else if is_web {
tracing::info!("starting in web server mode");
run_web_server()?;
} else {
// Default: run TUI single-process mode
tracing::info!("starting in TUI single-process mode");
run_tui_single_process()?;
}
Ok(())
}
/// Run the TUI in single-process mode (TUI + agent in one process).
fn run_tui_single_process() -> anyhow::Result<()> {
// Import and run the TUI's single-process entry point
zesdex_tui::run_single_process()
}
/// Run the REST API server.
fn run_api_server(args: &[String]) -> anyhow::Result<()> {
let port = args
.iter()
.position(|a| a == "--api-port")
.and_then(|i| args.get(i + 1))
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(8080);
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let store = zesdex_domain::core::Store::new();
let state = zesdex_api::ApiState::new(
store.base_dir.clone(),
"dev-secret",
"",
"deepseek-v4-flash-free",
Some("https://opencode.ai/zen/v1".to_string()),
);
let app = zesdex_api::build_router(state);
let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port));
tracing::info!("REST API server listening on {addr}");
println!("REST API server listening on http://{addr}/api/v1/health");
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok::<_, anyhow::Error>(())
})?;
Ok(())
}
/// Run the WebSocket server.
fn run_ws_server() -> anyhow::Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async { zesdex_ws::run_server(8081).await })?;
Ok(())
}
/// Run the gRPC server.
fn run_grpc_server() -> anyhow::Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async { zesdex_grpc::run_server(50051).await })?;
Ok(())
}
/// Serve the web frontend.
fn run_web_server() -> anyhow::Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async { zesdex_web::run_server(3000, None).await })?;
Ok(())
}