Skip to main content

kaizen/web/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Local daemon web app: embedded UI plus WebSocket tool calls.
3
4mod assets;
5mod event_display;
6pub mod features;
7mod live;
8mod prompt_cache;
9mod server;
10mod snapshot;
11mod token;
12pub mod tools;
13
14use crate::ipc::WebEndpoint;
15use anyhow::{Context, Result};
16use std::net::{IpAddr, Ipv4Addr, SocketAddr};
17use std::path::Path;
18use tokio::net::TcpListener;
19use tokio::task::JoinHandle;
20
21const DEFAULT_LISTEN: &str = "127.0.0.1:7878";
22
23pub async fn start(token_path: &Path) -> Result<(WebEndpoint, JoinHandle<()>)> {
24    let token = token::load_or_create_at(token_path)?;
25    let listener = bind_loopback().await?;
26    start_with_token(listener, token).await
27}
28
29pub async fn start_with_listener(listener: TcpListener) -> Result<(WebEndpoint, JoinHandle<()>)> {
30    start_with_token(listener, token::ephemeral()).await
31}
32
33pub async fn start_with_token(
34    listener: TcpListener,
35    token: String,
36) -> Result<(WebEndpoint, JoinHandle<()>)> {
37    let addr = listener.local_addr()?;
38    let endpoint = endpoint(addr, token);
39    let app = server::router(endpoint.token.clone());
40    let task = tokio::spawn(async move {
41        if let Err(err) = axum::serve(listener, app).await {
42            tracing::warn!(%err, "daemon web app stopped");
43        }
44    });
45    Ok((endpoint, task))
46}
47
48async fn bind_loopback() -> Result<TcpListener> {
49    match TcpListener::bind(DEFAULT_LISTEN).await {
50        Ok(listener) => Ok(listener),
51        Err(err) => bind_fallback()
52            .await
53            .with_context(|| format!("bind daemon web app at {DEFAULT_LISTEN}: {err}")),
54    }
55}
56
57async fn bind_fallback() -> Result<TcpListener> {
58    TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0))
59        .await
60        .map_err(Into::into)
61}
62
63fn endpoint(addr: SocketAddr, token: String) -> WebEndpoint {
64    let public = public_addr(addr);
65    WebEndpoint {
66        listen: addr.to_string(),
67        url: format!("http://{public}/?token={token}"),
68        token,
69    }
70}
71
72fn public_addr(addr: SocketAddr) -> SocketAddr {
73    if addr.ip().is_unspecified() {
74        return SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), addr.port());
75    }
76    addr
77}