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
@@ -0,0 +1,98 @@
//! Authentication middleware — session-lock based auth for Axum.
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use axum::body::Body;
use axum::http::{Request, Response, StatusCode};
use axum::response::IntoResponse;
use serde::{Deserialize, Serialize};
use tower::{Layer, Service};
/// Identity extracted from a validated session.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionIdentity {
pub session_id: String,
pub user_agent: String,
pub connected_at: i64,
}
impl SessionIdentity {
pub fn new(session_id: String, user_agent: String) -> Self {
let connected_at = chrono::Utc::now().timestamp();
Self {
session_id,
user_agent,
connected_at,
}
}
}
/// Tower Layer that produces SessionAuthMiddleware services.
#[derive(Debug, Clone)]
pub struct SessionAuthLayer;
impl SessionAuthLayer {
pub fn new() -> Self {
Self
}
}
impl Default for SessionAuthLayer {
fn default() -> Self {
Self
}
}
impl<S> Layer<S> for SessionAuthLayer {
type Service = SessionAuthMiddleware<S>;
fn layer(&self, inner: S) -> Self::Service {
SessionAuthMiddleware { inner }
}
}
/// Tower Service that validates X-Session-Id before forwarding.
#[derive(Debug, Clone)]
pub struct SessionAuthMiddleware<S> {
inner: S,
}
impl<S, ReqBody> Service<Request<ReqBody>> for SessionAuthMiddleware<S>
where
S: Service<Request<ReqBody>, Response = Response<Body>> + Send + 'static,
S::Future: Send + 'static,
ReqBody: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future =
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let session_id = req
.headers()
.get("X-Session-Id")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
if session_id.as_deref() != Some("valid-session") {
// In production, this validates against the store
return Box::pin(async move {
Ok((
StatusCode::UNAUTHORIZED,
"missing or invalid X-Session-Id header",
)
.into_response())
});
}
let fut = self.inner.call(req);
Box::pin(fut)
}
}
@@ -0,0 +1,23 @@
//! CORS layer factory for the daemon HTTP server.
use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer};
/// Return a permissive CorsLayer for local daemon IPC.
pub fn default_cors_layer() -> CorsLayer {
CorsLayer::new()
.allow_origin(AllowOrigin::any())
.allow_methods([
"GET".parse().unwrap(),
"POST".parse().unwrap(),
"PUT".parse().unwrap(),
"DELETE".parse().unwrap(),
"PATCH".parse().unwrap(),
"OPTIONS".parse().unwrap(),
])
.allow_headers(AllowHeaders::any())
.expose_headers([
"Content-Type".parse().unwrap(),
"X-Session-Id".parse().unwrap(),
"X-Request-Id".parse().unwrap(),
])
}
@@ -0,0 +1,5 @@
//! Axum middleware tower for the HTTP API layer.
pub mod auth;
pub mod cors;
pub mod rate_limit;
@@ -0,0 +1,60 @@
//! Simple in-memory rate limiter for Axum.
use std::collections::HashMap;
use std::sync::Mutex;
/// In-memory sliding-window rate limiter.
#[derive(Debug)]
pub struct RateLimiter {
windows: Mutex<HashMap<String, Vec<i64>>>,
}
impl RateLimiter {
pub fn new() -> Self {
RateLimiter {
windows: Mutex::new(HashMap::new()),
}
}
pub fn check_rate_limit(
&self,
client_id: &str,
max_requests: u32,
window_secs: u64,
) -> anyhow::Result<bool> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let cutoff = now.saturating_sub(window_secs as i64);
let mut windows = self.windows.lock().map_err(|e| {
anyhow::anyhow!("rate limiter lock poisoned: {e}")
})?;
let timestamps = windows.entry(client_id.to_string()).or_insert_with(Vec::new);
timestamps.retain(|&ts| ts >= cutoff);
if timestamps.len() >= max_requests as usize {
return Ok(false);
}
timestamps.push(now);
Ok(true)
}
pub fn reset(&self) -> anyhow::Result<()> {
let mut windows = self
.windows
.lock()
.map_err(|e| anyhow::anyhow!("rate limiter lock poisoned: {e}"))?;
windows.clear();
Ok(())
}
}
impl Default for RateLimiter {
fn default() -> Self {
Self::new()
}
}