Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 289 additions & 12 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ members = [
"crates/skills",
"crates/telegram",
"crates/tools",
"crates/tui",
"crates/voice",
]
resolver = "2"
Expand Down Expand Up @@ -69,6 +70,11 @@ tracing-opentelemetry = "0.30"
tracing-subscriber = { features = ["env-filter", "json"], version = "0.3" }
# CLI
clap = { features = ["derive", "env"], version = "4" }
# Terminal UI
crossterm = { features = ["event-stream"], version = "0.28" }
ratatui = "0.29"
rpassword = "5"
tui-textarea = "0.7"
# Database
sqlx = { features = ["migrate", "runtime-tokio", "sqlite"], version = "0.8" }
# HTTP client
Expand Down Expand Up @@ -111,7 +117,8 @@ teloxide = { features = ["macros"], version = "0.13" }
axum-server = { features = ["tls-rustls"], version = "0.7" }
rcgen = "0.13"
rustls = { features = ["ring"], version = "0.23" }
rustls-pemfile = "2"
rustls-native-certs = "0.8"
rustls-pemfile = "2"
time = "0.3"
tokio-rustls = "0.26"
# Cryptography / authentication
Expand Down Expand Up @@ -182,6 +189,7 @@ moltis-sessions = { path = "crates/sessions" }
moltis-skills = { path = "crates/skills" }
moltis-telegram = { path = "crates/telegram" }
moltis-tools = { path = "crates/tools" }
moltis-tui = { path = "crates/tui" }
moltis-voice = { path = "crates/voice" }

[profile.release]
Expand Down
3 changes: 3 additions & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ anyhow = { workspace = true }
clap = { workspace = true }
dotenvy = { workspace = true }
moltis-agents = { workspace = true }
moltis-tui = { optional = true, workspace = true }
moltis-browser = { workspace = true }
moltis-common = { workspace = true }
moltis-config = { workspace = true }
Expand Down Expand Up @@ -78,6 +79,7 @@ default = [
"qmd",
"tailscale",
"tls",
"tui",
"voice",
"web-ui",
]
Expand All @@ -93,6 +95,7 @@ push-notifications = ["moltis-gateway/push-notifications"]
qmd = ["moltis-gateway/qmd"]
tailscale = ["moltis-gateway/tailscale"]
tls = ["moltis-gateway/tls"]
tui = ["dep:moltis-tui"]
voice = ["moltis-gateway/voice"]
web-ui = ["moltis-gateway/web-ui"]

Expand Down
47 changes: 44 additions & 3 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,17 @@ enum Commands {
/// Install the Moltis CA certificate into the system trust store.
#[cfg(feature = "tls")]
TrustCa,
/// Launch the terminal user interface (connects to a running gateway).
#[cfg(feature = "tui")]
Tui {
/// Gateway WebSocket URL (e.g. wss://localhost:9433/ws/chat).
/// When omitted, derived from moltis.toml server config.
#[arg(long, env = "MOLTIS_URL")]
url: Option<String>,
/// API key for authentication.
#[arg(long, env = "MOLTIS_API_KEY")]
api_key: Option<String>,
},
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -184,7 +195,10 @@ enum SkillAction {

/// Initialise tracing and optionally attach a [`LogBroadcastLayer`] that
/// captures events into an in-memory ring buffer for the web UI.
fn init_telemetry(cli: &Cli, log_buffer: Option<LogBuffer>) {
///
/// When `tui_mode` is true, logs are written to `tui.log` inside the data
/// directory instead of stderr (which would corrupt the ratatui display).
fn init_telemetry(cli: &Cli, log_buffer: Option<LogBuffer>, tui_mode: bool) {
// Start with user-specified or default log level
let base_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&cli.log_level));
Expand All @@ -209,7 +223,23 @@ fn init_telemetry(cli: &Cli, log_buffer: Option<LogBuffer>) {
// Optionally attach the in-memory capture layer.
let log_layer = log_buffer.map(LogBroadcastLayer::new);

if cli.json_logs {
if tui_mode {
// TUI mode: redirect logs to a file so they don't corrupt the terminal.
let log_path = moltis_config::data_dir().join("tui.log");
let log_file = std::fs::File::create(&log_path).unwrap_or_else(|e| {
panic!("failed to create TUI log file {}: {e}", log_path.display())
});
registry
.with(
fmt::layer()
.with_writer(std::sync::Mutex::new(log_file))
.with_target(true)
.with_thread_ids(false)
.with_ansi(false),
)
.with(log_layer)
.init();
} else if cli.json_logs {
registry
.with(fmt::layer().json().with_target(true).with_thread_ids(false))
.with(log_layer)
Expand Down Expand Up @@ -315,7 +345,12 @@ async fn main() -> anyhow::Result<()> {
None
};

init_telemetry(&cli, log_buffer.clone());
#[cfg(feature = "tui")]
let tui_mode = matches!(cli.command, Some(Commands::Tui { .. }));
#[cfg(not(feature = "tui"))]
let tui_mode = false;

init_telemetry(&cli, log_buffer.clone(), tui_mode);

info!(version = env!("CARGO_PKG_VERSION"), "moltis starting");

Expand Down Expand Up @@ -403,6 +438,12 @@ async fn main() -> anyhow::Result<()> {
Some(Commands::Hooks { action }) => hooks_commands::handle_hooks(action).await,
#[cfg(feature = "tls")]
Some(Commands::TrustCa) => trust_ca().await,
#[cfg(feature = "tui")]
Some(Commands::Tui { url, api_key }) => {
moltis_tui::run_tui(url.as_deref(), api_key.as_deref())
.await
.map_err(Into::into)
},
Some(_) => {
eprintln!("command not yet implemented");
Ok(())
Expand Down
38 changes: 38 additions & 0 deletions crates/tui/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[package]
description = "Terminal user interface for Moltis gateway"
edition.workspace = true
name = "moltis-tui"
version.workspace = true

[dependencies]
crossterm = { workspace = true }
futures = { workspace = true }
moltis-config = { workspace = true }
moltis-protocol = { workspace = true }
pulldown-cmark = { workspace = true }
ratatui = { workspace = true }
reqwest = { workspace = true }
rustls = { workspace = true }
rustls-native-certs = { workspace = true }
rustls-pemfile = { workspace = true }
secrecy = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tokio-tungstenite = { workspace = true, features = ["rustls-tls-native-roots"] }
tracing = { workspace = true }
tui-textarea = { workspace = true }
url = { workspace = true }
uuid = { workspace = true }

[dev-dependencies]
tokio-test = { workspace = true }

[features]
default = []
metrics = []
tracing = []

[lints]
workspace = true
Loading