use std::io::Write;
use std::sync::Arc;
use std::time::Duration;
use bcode_client::{BcodeClient, DaemonAvailability};
use bcode_session_models::SessionId;
use bmux_tui::geometry::Rect;
use bmux_tui::terminal::Terminal;
use super::app::BmuxApp;
use super::effects::{TuiEffect, TuiEffectQueue};
use super::startup_action::StartupTuiAction;
use super::{TuiError, chat_loop, history_flow, root_program, session_flow};
#[allow(clippy::future_not_send, dead_code)]
pub async fn run_event_loop<W: Write>(
terminal: &mut Terminal<&mut W>,
session_id: Option<SessionId>,
) -> Result<(), TuiError> {
Box::pin(run_event_loop_with_startup(
terminal,
session_id,
StartupTuiAction::None,
))
.await
}
#[allow(clippy::future_not_send)]
pub async fn run_event_loop_with_static_bundled<W: Write>(
terminal: &mut Terminal<&mut W>,
session_id: Option<SessionId>,
static_plugins: &[bcode_plugin::StaticBundledPlugin],
launch_options: super::TuiLaunchOptions,
) -> Result<(), TuiError> {
Box::pin(run_event_loop_with_startup_and_static_bundled(
terminal,
session_id,
StartupTuiAction::None,
static_plugins,
launch_options,
))
.await
}
#[allow(clippy::future_not_send)]
pub async fn run_event_loop_with_startup<W: Write>(
terminal: &mut Terminal<&mut W>,
session_id: Option<SessionId>,
startup_action: StartupTuiAction,
) -> Result<(), TuiError> {
Box::pin(run_event_loop_with_startup_and_static_bundled(
terminal,
session_id,
startup_action,
&[],
super::TuiLaunchOptions::default(),
))
.await
}
#[allow(clippy::future_not_send)]
pub async fn run_standalone_plugin_surface<W: Write>(
terminal: &mut Terminal<&mut W>,
plugin_id: impl Into<String>,
surface: bcode_plugin_sdk::tui::BoxedPluginTuiSurface,
) -> Result<Option<serde_json::Value>, TuiError> {
let presentation = prepare_presentation(&super::static_bundled_plugins()).await?;
Box::pin(run_prepared_plugin_surface(
terminal,
plugin_id,
surface,
presentation,
))
.await
}
#[allow(clippy::future_not_send)]
pub async fn run_prepared_plugin_surface<W: Write>(
terminal: &mut Terminal<&mut W>,
plugin_id: impl Into<String>,
surface: bcode_plugin_sdk::tui::BoxedPluginTuiSurface,
presentation: PreparedPresentation,
) -> Result<Option<serde_json::Value>, TuiError> {
let plugin_id = plugin_id.into();
let (runtime, handle) = {
let InitializedTui {
client,
settings,
chat,
..
} = initialize_tui_with_presentation(
terminal.area(),
None,
&super::static_bundled_plugins(),
super::TuiLaunchOptions::default(),
presentation,
);
let passive_client = client
.clone()
.with_daemon_availability(DaemonAvailability::RequireRunning);
let loop_state =
chat_loop::ChatLoopState::new(&client, &passive_client, settings.metrics_enabled());
drop(passive_client);
drop(client);
let mut model = root_program::BcodeRuntimeModel::new(chat, settings, loop_state);
model.queue_standalone_plugin_surface(plugin_id.clone(), surface);
root_program::runtime(terminal, model)
};
let mut model = Box::pin(root_program::run(runtime, handle)).await?;
let outcome = model
.take_plugin_surface_result()
.filter(|(closed_plugin_id, _)| closed_plugin_id == &plugin_id)
.and_then(|(_, outcome)| outcome);
drop(model);
Ok(outcome)
}
#[allow(clippy::future_not_send)]
pub async fn run_event_loop_with_startup_and_static_bundled<W: Write>(
terminal: &mut Terminal<&mut W>,
session_id: Option<SessionId>,
startup_action: StartupTuiAction,
static_plugins: &[bcode_plugin::StaticBundledPlugin],
launch_options: super::TuiLaunchOptions,
) -> Result<(), TuiError> {
let connection = prepare_connection(session_id).await?;
Box::pin(run_prepared_chat(
terminal,
connection,
startup_action,
static_plugins,
launch_options,
))
.await
}
pub struct PreparedConnection {
session_id: Option<SessionId>,
publisher: Option<super::resurrection::Publisher>,
warning: Option<String>,
}
pub async fn prepare_connection(
session_id: Option<SessionId>,
) -> Result<PreparedConnection, TuiError> {
let config = bcode_config::load_config();
let (session_id, publisher, warning) = if let Ok(config) = &config {
super::resurrection::connect(&config.tui.session_resurrection, session_id).await?
} else {
(session_id, None, None)
};
Ok(PreparedConnection {
session_id,
publisher,
warning,
})
}
#[allow(clippy::future_not_send)]
pub async fn run_prepared_chat<W: Write>(
terminal: &mut Terminal<&mut W>,
connection: PreparedConnection,
startup_action: StartupTuiAction,
static_plugins: &[bcode_plugin::StaticBundledPlugin],
launch_options: super::TuiLaunchOptions,
) -> Result<(), TuiError> {
let PreparedConnection {
session_id,
publisher,
warning,
} = connection;
let mut initialized =
initialize_tui(terminal.area(), session_id, static_plugins, launch_options);
if let Some(warning) = warning {
initialized.chat.app.set_status(warning);
}
Box::pin(run_root(terminal, initialized, startup_action, publisher)).await
}
pub struct PreparedPresentation {
config: Result<bcode_config::BcodeConfig, bcode_config::ConfigError>,
streaming_override: Result<
Option<bcode_session_view_models::StreamingPresentationPolicy>,
bcode_config::ConfigError,
>,
presentation: Result<super::plugin_tui::PluginTuiPresentation, bcode_plugin::PluginLoadError>,
}
pub async fn prepare_presentation(
static_plugins: &[bcode_plugin::StaticBundledPlugin],
) -> Result<PreparedPresentation, TuiError> {
let plugins = static_plugins.to_vec();
Ok(tokio::task::spawn_blocking(move || load_presentation(&plugins)).await?)
}
fn load_presentation(static_plugins: &[bcode_plugin::StaticBundledPlugin]) -> PreparedPresentation {
let config = bcode_config::load_config();
let streaming_override = bcode_config::load_tui_streaming_presentation_override();
let presentation_config = config.as_ref().ok();
let selection =
presentation_config.map_or_else(bcode_plugin::PluginSelection::all_enabled, |config| {
let ids =
bcode_plugin::static_bundled_default_plugin_ids(static_plugins).unwrap_or_default();
bcode_config::plugin_selection_with_default_plugin_ids(config, &ids)
});
let visual = presentation_config
.map(|config| config.tui.visual_adapters.clone())
.unwrap_or_default();
let presentation = super::plugin_tui::load_default_presentation_with_static_bundled(
&selection,
visual,
static_plugins,
&super::bundled_tui_extensions(),
);
PreparedPresentation {
config,
streaming_override,
presentation,
}
}
#[allow(clippy::future_not_send)]
pub async fn run_prepared_chat_with_presentation<W: Write>(
terminal: &mut Terminal<&mut W>,
connection: PreparedConnection,
presentation: PreparedPresentation,
static_plugins: &[bcode_plugin::StaticBundledPlugin],
launch_options: super::TuiLaunchOptions,
) -> Result<(), TuiError> {
let mut initialized = initialize_tui_with_presentation(
terminal.area(),
connection.session_id,
static_plugins,
launch_options,
presentation,
);
if let Some(warning) = connection.warning {
initialized.chat.app.set_status(warning);
}
Box::pin(run_root(
terminal,
initialized,
StartupTuiAction::None,
connection.publisher,
))
.await
}
struct InitializedTui {
client: BcodeClient,
settings: chat_loop::TuiRuntimeSettings,
chat: session_flow::ActiveChat,
declarative_streaming_policy: bcode_session_view_models::StreamingPresentationPolicy,
streaming_presentation_override: Option<bcode_session_view_models::StreamingPresentationPolicy>,
}
fn initialize_tui(
terminal_area: Rect,
session_id: Option<SessionId>,
static_plugins: &[bcode_plugin::StaticBundledPlugin],
launch_options: super::TuiLaunchOptions,
) -> InitializedTui {
initialize_tui_with_presentation(
terminal_area,
session_id,
static_plugins,
launch_options,
load_presentation(static_plugins),
)
}
fn initialize_tui_with_presentation(
terminal_area: Rect,
session_id: Option<SessionId>,
static_plugins: &[bcode_plugin::StaticBundledPlugin],
launch_options: super::TuiLaunchOptions,
presentation: PreparedPresentation,
) -> InitializedTui {
let PreparedPresentation {
config,
streaming_override: streaming_presentation_override,
presentation,
} = presentation;
let client = config
.as_ref()
.map_or_else(
|_| BcodeClient::default_endpoint(),
|config| {
BcodeClient::default_endpoint()
.with_request_timeout(Duration::from_secs(config.client.request_timeout_secs))
},
)
.with_interaction_adapters(super::bundled_interaction_adapters("tui"));
let (event_sender, event_receiver) = history_flow::session_stream_channel();
let mut app = BmuxApp::new_with_history(session_id, &[], &[], false);
app.set_execution_mode_indicator(
match (launch_options.permission_mode, launch_options.tool_policy) {
(bcode_session_models::TurnPermissionMode::Bypass, _) => {
Some("DANGER: PERMISSION BYPASS ACTIVE".to_owned())
}
(_, bcode_session_models::TurnToolPolicy::Disabled) => {
Some("TOOLS DISABLED".to_owned())
}
_ => None,
},
);
match presentation {
Ok(presentation) => app.set_plugin_presentation(Arc::new(presentation)),
Err(error) => app.set_status(format!("plugin presentation unavailable: {error}")),
}
let agents = session_flow::AgentCatalog::default();
agents.refresh_app_agent_metadata(&mut app);
let launch_working_directory = std::env::current_dir().unwrap_or_else(|_| ".".into());
let mut settings =
chat_loop::TuiRuntimeSettings::bootstrap(launch_working_directory.clone(), static_plugins)
.with_launch_options(launch_options);
if let Ok(config) = &config {
settings.set_metrics_enabled(config.metrics.enabled);
}
let mut chat = session_flow::ActiveChat {
app,
agents,
attachment: session_flow::ChatSessionAttachment::Draft,
event_sender,
event_receiver,
event_task: None,
opening_session_progress: None,
pending_effects: TuiEffectQueue::default(),
};
let (declarative_streaming_policy, effective_streaming_override) = apply_initial_config(
config,
streaming_presentation_override,
session_id,
&launch_working_directory,
&mut settings,
&mut chat,
);
chat.start_effect(TuiEffect::LoadAgentCatalog);
if let Some(session_id) = session_id {
let initial_window_request = session_flow::initial_transcript_window_request(
super::render::transcript_area_for_frame(&chat.app, terminal_area),
);
session_flow::start_switch_session(&mut chat, session_id, initial_window_request);
} else {
chat.app.set_status("New draft".to_owned());
}
InitializedTui {
client,
settings,
chat,
declarative_streaming_policy,
streaming_presentation_override: effective_streaming_override,
}
}
fn apply_initial_config(
config: Result<bcode_config::BcodeConfig, bcode_config::ConfigError>,
streaming_override: Result<
Option<bcode_session_view_models::StreamingPresentationPolicy>,
bcode_config::ConfigError,
>,
session_id: Option<SessionId>,
launch_working_directory: &std::path::Path,
settings: &mut chat_loop::TuiRuntimeSettings,
chat: &mut session_flow::ActiveChat,
) -> (
bcode_session_view_models::StreamingPresentationPolicy,
Option<bcode_session_view_models::StreamingPresentationPolicy>,
) {
let Ok(config) = config else {
chat.start_effect(TuiEffect::LoadConfig);
return (
bcode_session_view_models::StreamingPresentationPolicy::default(),
None,
);
};
settings.apply_tui_config(&config.tui);
if let Err(error) = chat.app.apply_plugin_presentation_config(&config) {
chat.app.set_status(format!(
"plugin presentation configuration unavailable: {error}"
));
}
if let Some(presentation) = chat.app.plugin_presentation() {
settings.integrate_keymap_contributors(presentation.keymap_actions());
}
chat.app
.set_effective_keymap(settings.keymap().registry().clone());
settings.set_completion_settings(config.presentation.completion);
chat.app.apply_tui_config(config.tui.clone());
let declarative = config.presentation.streaming.policy();
let effective_override = match streaming_override {
Ok(streaming_override) => {
let effective = streaming_override.unwrap_or(declarative);
let _ = chat.app.apply_streaming_presentation_policy(effective);
streaming_override
}
Err(error) => {
let _ = chat.app.apply_streaming_presentation_policy(declarative);
chat.app
.set_status(format!("TUI streaming state unavailable: {error}"));
None
}
};
chat.start_effect(TuiEffect::ReconcileAuthSecurity {
config: Box::new(config),
});
if session_id.is_none() {
chat.start_effect(TuiEffect::LoadDraftStatus {
launch_working_directory: launch_working_directory.to_path_buf(),
});
}
(declarative, effective_override)
}
async fn run_root<W: Write>(
terminal: &mut Terminal<&mut W>,
initialized: InitializedTui,
startup_action: StartupTuiAction,
publisher: Option<super::resurrection::Publisher>,
) -> Result<(), TuiError> {
let passive_client = initialized
.client
.clone()
.with_daemon_availability(DaemonAvailability::RequireRunning);
let mut loop_state = chat_loop::ChatLoopState::new(
&initialized.client,
&passive_client,
initialized.settings.metrics_enabled(),
);
loop_state.declarative_streaming_policy = initialized.declarative_streaming_policy;
loop_state.streaming_presentation_override = initialized.streaming_presentation_override;
let (runtime, handle) = {
let mut model = root_program::BcodeRuntimeModel::new(
initialized.chat,
initialized.settings,
loop_state,
);
model.resurrection = publisher;
if let StartupTuiAction::OpenRalphHome { repo_path } = startup_action {
let surface =
super::ralph_launcher::open_root_ralph_home_surface(repo_path, None).await?;
model.queue_plugin_surface("bcode.ralph", surface);
}
root_program::runtime(terminal, model)
};
let _model = Box::pin(root_program::run(runtime, handle)).await?;
Ok(())
}