mod header_context;
mod local_agents;
mod persistent_memory;
mod resume_render;
use super::types::{BackgroundTaskGuard, SessionState, SessionUISetup};
use crate::agent::runloop::ResumeSession;
use crate::agent::runloop::unified::reasoning::{
model_supports_reasoning, resolve_reasoning_visibility,
};
use crate::agent::runloop::unified::session_setup::ide_context::IdeContextBridge;
use crate::agent::runloop::unified::stop_requests::request_local_stop;
use crate::agent::runloop::unified::turn::utils::{
append_additional_context, render_hook_messages,
};
use crate::agent::runloop::unified::turn::workspace::load_workspace_files;
use crate::agent::runloop::unified::{context_manager, state};
use anyhow::{Context, Result};
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use tokio::sync::{Notify, mpsc::UnboundedSender};
use tracing::warn;
use vtcode_core::config::constants::ui;
use vtcode_core::config::loader::VTCodeConfig;
use vtcode_core::config::types::AgentConfig as CoreAgentConfig;
use vtcode_core::core::agent::steering::SteeringMessage;
use vtcode_core::hooks::{LifecycleHookEngine, SessionEndReason, SessionStartTrigger};
use vtcode_core::notifications::{
set_global_notification_hook_engine, set_global_terminal_focused,
};
use vtcode_core::prompts::discover_prompt_templates;
use vtcode_core::ui::slash::visible_commands;
use vtcode_core::ui::theme;
use vtcode_core::ui::{
inline_theme_from_core_styles, is_tui_mode, set_tui_mode, to_tui_appearance, to_tui_fullscreen,
to_tui_keyboard_protocol, to_tui_slash_commands, to_tui_surface,
};
use vtcode_core::utils::ansi::{AnsiRenderer, MessageStyle};
use vtcode_core::utils::session_archive::SessionArchive;
use vtcode_core::utils::transcript;
use vtcode_tui::app::{
AgentPaletteItem, FocusChangeCallback, InlineEvent, InlineEventCallback, SessionOptions,
SlashCommandItem, spawn_session_with_options,
};
use self::header_context::{HeaderContextInit, initialize_header_context};
pub(crate) use self::header_context::{
apply_ide_context_snapshot, ide_context_status_label_from_bridge,
};
pub(crate) use self::local_agents::refresh_local_agents;
use self::resume_render::render_resume_state_if_present;
#[cfg(test)]
use self::header_context::ide_context_status_label;
#[cfg(test)]
use self::local_agents::{
background_local_agent_preview_placeholder, delegated_local_agent_preview_placeholder,
visible_background_local_agents, visible_delegated_local_agents,
};
#[cfg(test)]
use self::persistent_memory::apply_persistent_memory_header_guide;
#[cfg(test)]
use self::persistent_memory::{persistent_memory_guide_lines, persistent_memory_header_badge};
#[cfg(test)]
use self::resume_render::{build_structured_resume_lines, infer_legacy_line_style};
#[cfg(test)]
use vtcode_core::llm::provider as uni;
#[cfg(test)]
use vtcode_core::persistent_memory::PersistentMemoryStatus;
#[cfg(test)]
use vtcode_core::subagents::SubagentStatusEntry;
#[cfg(test)]
use vtcode_tui::app::InlineHeaderContext;
#[cfg(test)]
use vtcode_tui::app::InlineHeaderStatusTone;
pub(crate) struct SessionUiLaunchOptions {
pub session_archive: Option<SessionArchive>,
pub full_auto: bool,
pub skip_confirmations: bool,
pub steering_sender: Option<UnboundedSender<SteeringMessage>>,
}
pub(crate) async fn initialize_session_ui(
config: &CoreAgentConfig,
vt_cfg: Option<&VTCodeConfig>,
session_id: &str,
session_state: &mut SessionState,
session_trigger: SessionStartTrigger,
resume_state: Option<&ResumeSession>,
options: SessionUiLaunchOptions,
) -> Result<SessionUISetup> {
let SessionUiLaunchOptions {
session_archive,
full_auto,
skip_confirmations,
steering_sender,
} = options;
let lifecycle_hooks = if let Some(vt) = vt_cfg {
LifecycleHookEngine::new_with_session(
config.workspace.clone(),
&vt.hooks,
session_trigger,
session_id,
vt.permissions.default_mode,
)?
} else {
None
};
set_global_notification_hook_engine(lifecycle_hooks.clone());
let mut context_manager = context_manager::ContextManager::new(
session_state.base_system_prompt.clone(),
(),
session_state.loaded_skills.clone(),
vt_cfg.map(|cfg| cfg.agent.clone()),
);
context_manager.set_workspace_root(config.workspace.as_path());
let active_styles = theme::active_styles();
let theme_spec = inline_theme_from_core_styles(&active_styles);
let default_placeholder = session_state
.session_bootstrap
.placeholder
.clone()
.or_else(|| Some(ui::CHAT_INPUT_PLACEHOLDER_BOOTSTRAP.to_string()));
let follow_up_placeholder = if session_state.session_bootstrap.placeholder.is_none() {
Some(ui::CHAT_INPUT_PLACEHOLDER_FOLLOW_UP.to_string())
} else {
None
};
let inline_rows = vt_cfg
.as_ref()
.map(|cfg| cfg.ui.inline_viewport_rows)
.unwrap_or(ui::DEFAULT_INLINE_VIEWPORT_ROWS);
if !is_tui_mode() {
set_tui_mode(true);
}
let ctrl_c_state = Arc::new(state::CtrlCState::new());
let ctrl_c_notify = Arc::new(Notify::new());
let input_activity_counter = Arc::new(AtomicU64::new(0));
let interrupt_callback: InlineEventCallback = {
let state = ctrl_c_state.clone();
let notify = ctrl_c_notify.clone();
let steering_sender = steering_sender.clone();
Arc::new(move |event: &InlineEvent| match event {
InlineEvent::Interrupt => {
let _ = request_local_stop(&state, ¬ify);
}
InlineEvent::Pause => {
if let Some(sender) = steering_sender.as_ref() {
let _ = sender.send(SteeringMessage::Pause);
}
}
InlineEvent::Resume => {
if let Some(sender) = steering_sender.as_ref() {
let _ = sender.send(SteeringMessage::Resume);
}
}
InlineEvent::Steer(text) => {
if let Some(sender) = steering_sender.as_ref() {
let _ = sender.send(SteeringMessage::FollowUpInput(text.clone()));
}
}
_ => {}
})
};
let focus_callback: FocusChangeCallback = Arc::new(set_global_terminal_focused);
let pty_counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
session_state
.tool_registry
.set_active_pty_sessions(pty_counter.clone());
let visible_slash_commands: Vec<_> = visible_commands().into_iter().copied().collect();
let mut slash_command_items = to_tui_slash_commands(visible_slash_commands.as_slice());
let template_slash_commands = discover_prompt_templates(&config.workspace)
.await
.into_iter()
.filter(|template| {
!visible_slash_commands
.iter()
.any(|cmd| cmd.name == template.name)
})
.map(|template| SlashCommandItem::new(template.name, template.description))
.collect::<Vec<_>>();
slash_command_items.extend(template_slash_commands);
let mut session = spawn_session_with_options(
theme_spec.clone(),
SessionOptions {
placeholder: default_placeholder.clone(),
surface_preference: vt_cfg
.and_then(|cfg| cfg.tui.alternate_screen)
.map(|mode| match mode {
vtcode_core::config::TuiAlternateScreen::Always => {
vtcode_tui::app::SessionSurface::Alternate
}
vtcode_core::config::TuiAlternateScreen::Never => {
vtcode_tui::app::SessionSurface::Inline
}
})
.unwrap_or_else(|| to_tui_surface(config.ui_surface)),
inline_rows,
event_callback: Some(interrupt_callback),
focus_callback: Some(focus_callback),
active_pty_sessions: Some(pty_counter.clone()),
input_activity_counter: Some(input_activity_counter.clone()),
keyboard_protocol: vt_cfg
.map(|cfg| to_tui_keyboard_protocol(cfg.ui.keyboard_protocol.clone()))
.unwrap_or_default(),
fullscreen: vt_cfg.map(to_tui_fullscreen).unwrap_or_default(),
workspace_root: Some(config.workspace.clone()),
slash_commands: slash_command_items,
appearance: vt_cfg.map(to_tui_appearance),
app_name: "VT Code".to_string(),
non_interactive_hint: Some(
"Use `vtcode ask \"your prompt\"` for non-interactive input.".to_string(),
),
},
)
.context("failed to launch inline session")?;
set_global_terminal_focused(true);
if skip_confirmations {
session.set_skip_confirmations(true);
}
let handle = session.clone_inline_handle();
let highlight_config = vt_cfg
.as_ref()
.map(|cfg| cfg.syntax_highlighting.clone())
.unwrap_or_default();
transcript::set_inline_handle(Arc::new(handle.clone()));
let mut ide_context_bridge = Some(IdeContextBridge::new(config.workspace.clone()));
let mut renderer = AnsiRenderer::with_inline_ui(handle.clone(), highlight_config);
let supports_reasoning =
model_supports_reasoning(&*session_state.provider_client, &config.model);
renderer.set_reasoning_visible(resolve_reasoning_visibility(vt_cfg, supports_reasoning));
if let Some(cfg) = vt_cfg {
renderer.set_screen_reader_mode(cfg.ui.screen_reader_mode);
renderer.set_show_diagnostics_in_transcript(cfg.ui.show_diagnostics_in_transcript);
}
let workspace_for_indexer = config.workspace.clone();
let workspace_for_palette = config.workspace.clone();
let handle_for_indexer = handle.clone();
let file_palette_task_guard = BackgroundTaskGuard::new(tokio::spawn(async move {
match load_workspace_files(workspace_for_indexer).await {
Ok(files) => {
if !files.is_empty() {
handle_for_indexer.configure_file_palette(files, workspace_for_palette);
} else {
tracing::debug!("No files found in workspace for file palette");
}
}
Err(err) => {
tracing::warn!("Failed to load workspace files for file palette: {}", err);
}
}
}));
let mut background_subprocess_task_guard = None;
if let Some(controller) = session_state.tool_registry.subagent_controller() {
let handle_for_agents = handle.clone();
let controller_for_agents = controller.clone();
tokio::spawn(async move {
let specs = controller_for_agents.effective_specs().await;
if specs.is_empty() {
return;
}
handle_for_agents.configure_agent_palette(
specs
.into_iter()
.map(|spec| AgentPaletteItem {
name: spec.name,
description: Some(spec.description),
})
.collect(),
);
});
let handle_for_subprocesses = handle.clone();
let controller_for_subprocesses = controller.clone();
let refresh_interval_ms = vt_cfg
.map(|cfg| cfg.subagents.background.refresh_interval_ms)
.unwrap_or(2_000)
.max(250);
background_subprocess_task_guard =
Some(BackgroundTaskGuard::new(tokio::spawn(async move {
let mut interval =
tokio::time::interval(std::time::Duration::from_millis(refresh_interval_ms));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
interval.tick().await;
if let Err(err) =
refresh_local_agents(&handle_for_subprocesses, &controller_for_subprocesses)
.await
{
tracing::warn!("Failed to refresh background subprocesses: {}", err);
}
}
})));
}
transcript::clear();
render_resume_state_if_present(&mut renderer, resume_state, supports_reasoning)?;
let provider_label = {
let label = super::init::resolve_provider_label(config, vt_cfg);
if label.is_empty() {
session_state.provider_client.name().to_string()
} else {
label
}
};
let header_provider_label = provider_label.clone();
let mut checkpoint_config =
vtcode_core::core::agent::snapshots::SnapshotConfig::new(config.workspace.clone());
checkpoint_config.enabled = config.checkpointing_enabled;
checkpoint_config.storage_dir = config.checkpointing_storage_dir.clone();
checkpoint_config.max_snapshots = config.checkpointing_max_snapshots;
checkpoint_config.max_age_days = config.checkpointing_max_age_days;
let checkpoint_manager =
match vtcode_core::core::agent::snapshots::SnapshotManager::new(checkpoint_config) {
Ok(manager) => Some(manager),
Err(err) => {
warn!("Failed to initialize checkpoint manager: {}", err);
None
}
};
if let (Some(hooks), Some(archive)) = (&lifecycle_hooks, session_archive.as_ref()) {
hooks
.update_transcript_path(Some(archive.path().to_path_buf()))
.await;
}
if let Some(hooks) = &lifecycle_hooks {
match hooks.run_session_start().await {
Ok(outcome) => {
render_hook_messages(&mut renderer, &outcome.messages)?;
append_additional_context(
&mut session_state.conversation_history,
outcome.additional_context,
);
}
Err(err) => {
renderer.line(
MessageStyle::Error,
&format!("Failed to run session start hooks: {}", err),
)?;
}
}
}
if full_auto && let Some(allowlist) = session_state.full_auto_allowlist.as_ref() {
if allowlist.is_empty() {
renderer.line(
MessageStyle::Info,
"Full-auto mode enabled with no tool permissions; tool calls will be skipped.",
)?;
} else {
renderer.line(
MessageStyle::Info,
&format!(
"Full-auto mode enabled. Permitted tools: {}",
allowlist.join(", ")
),
)?;
}
}
handle.set_placeholder(default_placeholder.clone());
let header_context = initialize_header_context(
&mut renderer,
&handle,
&mut context_manager,
&mut ide_context_bridge,
HeaderContextInit {
config,
vt_cfg,
session_bootstrap: &session_state.session_bootstrap,
provider_client: &*session_state.provider_client,
header_provider_label,
full_auto,
},
)
.await?;
let mut startup_update_notice_rx = None;
let mut startup_update_task_guard = None;
if session_state.startup_update_check.should_refresh {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let announced_version = session_state
.startup_update_check
.cached_notice
.as_ref()
.map(|notice| notice.latest_version.clone());
startup_update_notice_rx = Some(rx);
startup_update_task_guard = Some(BackgroundTaskGuard::new(tokio::spawn(async move {
let updater = match crate::updater::Updater::new(env!("CARGO_PKG_VERSION")) {
Ok(updater) => updater,
Err(err) => {
tracing::debug!("Failed to initialize updater in background task: {}", err);
return;
}
};
match updater.refresh_startup_update_cache().await {
Ok(Some(notice)) if Some(notice.latest_version.clone()) != announced_version => {
let _ = tx.send(notice);
}
Ok(_) => {}
Err(err) => {
tracing::debug!("Background startup update refresh failed: {}", err);
}
}
})));
}
let next_checkpoint_turn = checkpoint_manager
.as_ref()
.and_then(|manager| manager.next_turn_number().ok())
.unwrap_or(1);
Ok(SessionUISetup {
renderer,
session,
handle,
header_context,
ide_context_bridge,
ctrl_c_state,
ctrl_c_notify,
input_activity_counter,
checkpoint_manager,
session_archive,
lifecycle_hooks,
session_end_reason: SessionEndReason::Completed,
context_manager,
default_placeholder,
follow_up_placeholder,
next_checkpoint_turn,
file_palette_task_guard,
background_subprocess_task_guard,
startup_update_cached_notice: session_state.startup_update_check.cached_notice.clone(),
startup_update_notice_rx,
startup_update_task_guard,
})
}
#[cfg(test)]
mod tests;