#[cfg(feature = "tui")]
mod app;
#[cfg(feature = "tui")]
mod cache;
#[cfg(feature = "tui")]
pub mod chat_agent;
#[cfg(feature = "tui")]
pub mod command;
#[cfg(feature = "tui")]
pub mod config;
#[cfg(feature = "tui")]
mod cosmic_theme;
#[cfg(feature = "tui")]
mod edit_history;
#[cfg(feature = "tui")]
pub mod file_resolve;
#[cfg(feature = "tui")]
mod focus;
#[cfg(feature = "tui")]
pub mod icons;
#[cfg(feature = "tui")]
mod keybindings;
#[cfg(feature = "tui")]
mod layout;
#[cfg(feature = "tui")]
mod mode;
#[cfg(feature = "tui")]
pub mod session;
#[cfg(feature = "tui")]
mod standalone;
#[cfg(feature = "tui")]
pub mod startup;
#[cfg(feature = "tui")]
mod state;
#[cfg(feature = "tui")]
mod theme;
#[cfg(feature = "tui")]
pub mod tokens;
#[cfg(feature = "tui")]
mod unicode;
#[cfg(feature = "tui")]
mod utils;
#[cfg(feature = "tui")]
mod verification;
#[cfg(feature = "tui")]
mod views;
#[cfg(feature = "tui")]
pub mod widgets;
#[cfg(feature = "tui")]
pub use app::App;
#[cfg(feature = "tui")]
pub use cache::RenderCache;
#[cfg(feature = "tui")]
pub use chat_agent::{ChatAgent, ChatMessage, ChatRole, StreamingState};
#[cfg(feature = "tui")]
pub use command::{Command, HELP_TEXT};
#[cfg(feature = "tui")]
pub use config::{
ChatSettings, ConfigError, PathSettings, StudioSettings, ThemeName, TuiConfig, TuiSettings,
};
#[cfg(feature = "tui")]
pub use cosmic_theme::CosmicTheme;
#[cfg(feature = "tui")]
pub use edit_history::EditHistory;
#[cfg(feature = "tui")]
pub use file_resolve::FileResolver;
#[cfg(feature = "tui")]
pub use focus::{FocusState, PanelId as NavPanelId};
#[cfg(feature = "tui")]
pub use icons::{IconMode, IconSet};
#[cfg(feature = "tui")]
pub use keybindings::{format_key, keybindings_for_context, KeyCategory, Keybinding};
#[cfg(feature = "tui")]
pub use layout::{LayoutMode, ResponsiveLayout};
#[cfg(feature = "tui")]
pub use mode::InputMode;
#[cfg(feature = "tui")]
pub use session::{
delete_session, get_latest_session, list_sessions, load_session, save_session, ChatSession,
SessionMeta,
};
#[cfg(feature = "tui")]
pub use standalone::{BrowserEntry, HistoryEntry, StandalonePanel, StandaloneState};
#[cfg(feature = "tui")]
pub use state::{
AgentTurnState,
PanelId,
PanelScrollState,
TuiMode,
TuiState,
FRAME_CYCLE,
FRAME_DIV_GLACIAL,
FRAME_DIV_NORMAL,
TARGET_FPS,
};
#[cfg(feature = "tui")]
pub use theme::{ColorMode, MissionPhase, TaskStatus, Theme, VerbColor};
#[cfg(feature = "tui")]
pub use tokens::{ColorPalette, CosmicVariant, SemanticColors, TokenResolver};
#[cfg(feature = "tui")]
pub use unicode::{display_width, truncate_to_width};
#[cfg(feature = "tui")]
pub use utils::{format_number, format_number_compact, format_number_u64};
#[cfg(feature = "tui")]
pub use verification::{VerificationCache, VerificationEntry};
#[cfg(feature = "tui")]
pub use views::{
ChatView, DagTab, HelpView, HomeView, MissionTab, MonitorView, NovanetTab, ReasoningTab,
SettingsView, StudioView, TuiView, View, ViewAction,
};
#[cfg(feature = "tui")]
fn install_panic_hook() {
use std::io::Write;
use std::panic;
use std::sync::Once;
use crossterm::{execute, terminal::LeaveAlternateScreen};
static HOOK_INSTALLED: Once = Once::new();
HOOK_INSTALLED.call_once(|| {
let original_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
let _ = crossterm::terminal::disable_raw_mode();
let _ = execute!(std::io::stdout(), LeaveAlternateScreen);
let crash_log_path = dirs::config_dir()
.map(|d| d.join("nika").join("crash.log"))
.unwrap_or_else(|| std::path::PathBuf::from("/tmp/nika-crash.log"));
if let Some(parent) = crash_log_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&crash_log_path)
{
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
let _ = writeln!(
f,
"\n══════════════════════════════════════════════════════════════"
);
let _ = writeln!(f, "NIKA CRASH: {}", timestamp);
let _ = writeln!(
f,
"══════════════════════════════════════════════════════════════"
);
let _ = writeln!(f, "{}", panic_info);
let backtrace = std::backtrace::Backtrace::capture();
let _ = writeln!(f, "\nBacktrace:\n{}", backtrace);
}
eprintln!(
"\n\x1b[31m╔══════════════════════════════════════════════════════════════╗\x1b[0m"
);
eprintln!(
"\x1b[31m║ NIKA CRASHED - Terminal has been restored ║\x1b[0m"
);
eprintln!(
"\x1b[31m╠══════════════════════════════════════════════════════════════╣\x1b[0m"
);
eprintln!(
"\x1b[31m║ Crash log: {:49} ║\x1b[0m",
crash_log_path.display()
);
eprintln!(
"\x1b[31m║ Please report: https://github.com/SuperNovae-studio/nika ║\x1b[0m"
);
eprintln!(
"\x1b[31m╚══════════════════════════════════════════════════════════════╝\x1b[0m"
);
original_hook(panic_info);
}));
});
}
#[cfg(feature = "tui")]
pub async fn run_tui(workflow_path: &std::path::Path) -> crate::error::Result<()> {
use crate::ast::Workflow;
use crate::event::EventLog;
use crate::runtime::Runner;
use crate::serde_yaml;
install_panic_hook();
let yaml_content = tokio::fs::read_to_string(workflow_path)
.await
.map_err(|e| crate::error::NikaError::WorkflowNotFound {
path: format!("{}: {}", workflow_path.display(), e),
})?;
let workflow: Workflow = serde_yaml::from_str(&yaml_content).map_err(|e| {
let line_info = e
.location()
.map(|l| format!(" (line {})", l.line()))
.unwrap_or_default();
crate::error::NikaError::ParseError {
details: format!("{}{}", e, line_info),
}
})?;
workflow.validate_schema()?;
let (event_log, event_rx) = EventLog::new_with_broadcast();
let mut runner = Runner::with_event_log(workflow, event_log).quiet();
let runner_handle = tokio::spawn(async move {
match runner.run().await {
Ok(output) => {
tracing::info!("Workflow completed: {} bytes output", output.len());
}
Err(e) => {
tracing::error!("Workflow failed: {}", e);
}
}
});
let app = App::new(workflow_path)?.with_broadcast_receiver(event_rx);
let tui_result = app.run_unified().await;
runner_handle.abort();
tui_result
}
#[cfg(feature = "tui")]
pub async fn run_tui_standalone() -> crate::error::Result<()> {
install_panic_hook();
let root = find_project_root().unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
let state = StandaloneState::new(root);
let app = App::new_standalone(state)?;
app.run_unified().await
}
#[cfg(feature = "tui")]
pub async fn run_tui_chat(
provider: Option<String>,
model: Option<String>,
) -> crate::error::Result<()> {
use views::TuiView;
install_panic_hook();
let root = find_project_root().unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
let state = StandaloneState::new(root);
let app = App::new_standalone(state)?
.with_initial_view(TuiView::Chat)
.with_chat_overrides(provider, model);
app.run_unified().await
}
#[cfg(feature = "tui")]
pub async fn run_tui_studio(workflow: Option<std::path::PathBuf>) -> crate::error::Result<()> {
use views::TuiView;
install_panic_hook();
let root = find_project_root().unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
let state = StandaloneState::new(root.clone());
let mut app = App::new_standalone(state)?.with_initial_view(TuiView::Editor);
if let Some(path) = workflow {
let full_path = if path.is_absolute() {
path
} else {
root.join(path)
};
app = app.with_studio_file(full_path);
}
app.run_unified().await
}
#[cfg(feature = "tui")]
pub async fn run_tui_with_options(
workflow: Option<std::path::PathBuf>,
initial_view: Option<views::TuiView>,
) -> crate::error::Result<()> {
use views::TuiView;
install_panic_hook();
let root = find_project_root().unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
let state = StandaloneState::new(root.clone());
let mut app = App::new_standalone(state)?;
if let Some(view) = initial_view {
app = app.with_initial_view(view);
}
if let Some(path) = workflow {
let full_path = if path.is_absolute() {
path
} else {
root.join(path)
};
match initial_view {
Some(TuiView::Editor) | Some(TuiView::Split) | None => {
app = app.with_studio_file(full_path);
}
_ => {
app = app.with_studio_file(full_path);
}
}
}
app.run_unified().await
}
#[cfg(feature = "tui")]
fn find_project_root() -> Option<std::path::PathBuf> {
let mut current = std::env::current_dir().ok()?;
loop {
if current.join("Cargo.toml").exists() || current.join(".git").exists() {
return Some(current);
}
if !current.pop() {
return None;
}
}
}
#[cfg(not(feature = "tui"))]
pub async fn run_tui(_workflow_path: &std::path::Path) -> crate::error::Result<()> {
Err(crate::error::NikaError::ValidationError {
reason: "TUI feature not enabled. Rebuild with --features tui".to_string(),
})
}
#[cfg(not(feature = "tui"))]
pub async fn run_tui_standalone() -> crate::error::Result<()> {
Err(crate::error::NikaError::ValidationError {
reason: "TUI feature not enabled. Rebuild with --features tui".to_string(),
})
}
#[cfg(not(feature = "tui"))]
pub async fn run_tui_chat(
_provider: Option<String>,
_model: Option<String>,
) -> crate::error::Result<()> {
Err(crate::error::NikaError::ValidationError {
reason: "TUI feature not enabled. Rebuild with --features tui".to_string(),
})
}
#[cfg(not(feature = "tui"))]
pub async fn run_tui_studio(_workflow: Option<std::path::PathBuf>) -> crate::error::Result<()> {
Err(crate::error::NikaError::ValidationError {
reason: "TUI feature not enabled. Rebuild with --features tui".to_string(),
})
}
#[cfg(not(feature = "tui"))]
pub async fn run_tui_with_options(
_workflow: Option<std::path::PathBuf>,
_initial_view: Option<views::TuiView>,
) -> crate::error::Result<()> {
Err(crate::error::NikaError::ValidationError {
reason: "TUI feature not enabled. Rebuild with --features tui".to_string(),
})
}