use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use super::progress::ProgressReporter;
use anstyle::Effects;
use anyhow::Result;
use tokio::sync::{Notify, RwLock, watch};
use tokio::task;
use tokio::time::MissedTickBehavior;
use vtcode_commons::stop_hints::with_stop_hint;
use vtcode_core::config::api_keys::{ApiKeySources, get_api_key};
use vtcode_core::config::loader::layers::ConfigLayerSource;
use vtcode_core::config::loader::{ConfigManager, VTCodeConfig};
use vtcode_core::config::mcp::McpTransportConfig;
use vtcode_core::config::types::AgentConfig as CoreAgentConfig;
use vtcode_core::copilot::{CopilotAuthStatusKind, probe_auth_status};
use vtcode_core::instructions::{InstructionSourceKind, format_instruction_path};
use vtcode_core::llm::provider as uni;
use vtcode_core::project_doc::load_instruction_appendix;
use vtcode_core::utils::ansi::{AnsiRenderer, MessageStyle};
use vtcode_terminal_detection::TerminalType;
use vtcode_tui::app::InlineHandle;
use super::async_mcp_manager::{AsyncMcpManager, McpInitStatus};
use super::state::{CtrlCState, SessionStats};
use super::status_line::InputStatusState;
type LoadedSkillsMap = hashbrown::HashMap<String, vtcode_core::skills::types::Skill>;
pub(crate) struct SessionStatusContext<'a> {
pub config: &'a CoreAgentConfig,
pub vt_cfg: Option<&'a VTCodeConfig>,
pub active_instruction_directory: &'a Path,
pub instruction_context_paths: &'a [PathBuf],
pub message_count: usize,
pub stats: &'a SessionStats,
pub available_tools: usize,
pub async_mcp_manager: Option<&'a AsyncMcpManager>,
pub loaded_skills: &'a Arc<RwLock<LoadedSkillsMap>>,
}
pub(crate) async fn display_session_status(
renderer: &mut AnsiRenderer,
ctx: SessionStatusContext<'_>,
) -> Result<()> {
let openai_session = match crate::cli::auth::refresh_openai_login_if_available(ctx.vt_cfg).await
{
Ok(session) => session,
Err(err) => {
renderer.line(
MessageStyle::Warning,
&format!("OpenAI OAuth refresh failed during /status: {}", err),
)?;
crate::cli::auth::load_openai_session(ctx.vt_cfg)?
}
};
let session_id = current_session_id();
let ide_info = detect_ide_info();
let terminal_info = detect_terminal_info();
let mcp_info = summarize_mcp_servers(ctx.vt_cfg, ctx.async_mcp_manager).await;
let skills_info = summarize_loaded_skills(ctx.loaded_skills).await;
let agents_info = summarize_loaded_agents_sources(
ctx.vt_cfg,
ctx.active_instruction_directory,
ctx.instruction_context_paths,
)
.await;
let memory_info = summarize_project_memory(&ctx.config.workspace);
let settings_info = summarize_setting_sources(&ctx.config.workspace);
render_status_heading(renderer, "Session status")?;
renderer.line(
MessageStyle::Info,
&format!(" Version: {}", env!("CARGO_PKG_VERSION")),
)?;
renderer.line(MessageStyle::Info, &format!(" Session ID: {}", session_id))?;
renderer.line(
MessageStyle::Info,
&format!(" Directory: {}", ctx.config.workspace.display()),
)?;
renderer.line(MessageStyle::Info, "")?;
let provider_label = {
let label = crate::agent::runloop::unified::session_setup::resolve_provider_label(
ctx.config, ctx.vt_cfg,
);
if label.is_empty() {
ctx.config.provider.clone()
} else {
label
}
};
renderer.line(
MessageStyle::Info,
&format!(" Provider: {}", provider_label),
)?;
renderer.line(
MessageStyle::Info,
&format!(" Model: {}", ctx.config.model),
)?;
render_auth_usage_status(renderer, &ctx, openai_session.as_ref()).await?;
renderer.line(MessageStyle::Info, &format!(" IDE: {}", ide_info))?;
renderer.line(
MessageStyle::Info,
&format!(" Terminal: {}", terminal_info),
)?;
renderer.line(MessageStyle::Info, &format!(" MCP servers: {}", mcp_info))?;
renderer.line(MessageStyle::Info, &format!(" Skills: {}", skills_info))?;
renderer.line(
MessageStyle::Info,
&format!(" Loaded AGENTS.md sources: {}", agents_info),
)?;
renderer.line(MessageStyle::Info, "")?;
renderer.line(MessageStyle::Info, &format!(" Memory: {}", memory_info))?;
renderer.line(
MessageStyle::Info,
&format!(" Setting sources: {}", settings_info),
)?;
renderer.line(
MessageStyle::Info,
&format!(" Reasoning effort: {}", ctx.config.reasoning_effort),
)?;
renderer.line(
MessageStyle::Info,
&format!(" Messages so far: {}", ctx.message_count),
)?;
let used_tools = ctx.stats.sorted_tools();
if used_tools.is_empty() {
renderer.line(
MessageStyle::Info,
&format!(" Tools used: 0 / {}", ctx.available_tools),
)?;
} else {
renderer.line(
MessageStyle::Info,
&format!(
" Tools used: {} / {} ({})",
used_tools.len(),
ctx.available_tools,
used_tools.join(", ")
),
)?;
}
Ok(())
}
async fn render_auth_usage_status(
renderer: &mut AnsiRenderer,
ctx: &SessionStatusContext<'_>,
openai_session: Option<&vtcode_auth::OpenAIChatGptSession>,
) -> Result<()> {
let openrouter_status = crate::cli::auth::openrouter_auth_status(ctx.vt_cfg)?;
let openai_status = crate::cli::auth::openai_auth_status(ctx.vt_cfg)?;
let copilot_auth_cfg = ctx
.vt_cfg
.map(|cfg| cfg.auth.copilot.clone())
.unwrap_or_default();
let copilot_status = probe_auth_status(&copilot_auth_cfg, Some(&ctx.config.workspace)).await;
let openai_connected = matches!(
openai_status,
vtcode_auth::OpenAIChatGptAuthStatus::Authenticated { .. }
) || ctx.config.openai_chatgpt_auth.is_some();
render_status_heading(renderer, "Authentication info")?;
renderer.line(
MessageStyle::Info,
&format!(
" OpenAI: {}",
if openai_connected {
"connected"
} else {
"not connected"
}
),
)?;
if let Some(session) = openai_session {
if let Some(email) = session.email.as_deref() {
renderer.line(MessageStyle::Info, &format!(" Account: {}", email))?;
}
if let Some(plan) = session.plan.as_deref() {
renderer.line(MessageStyle::Info, &format!(" Plan: {}", plan))?;
}
}
renderer.line(
MessageStyle::Info,
&format!(
" OpenRouter: {}",
short_oauth_status_openrouter(&openrouter_status)
),
)?;
renderer.line(
MessageStyle::Info,
&format!(
" GitHub Copilot: {}",
short_copilot_status(&copilot_status)
),
)?;
if ctx.config.provider.eq_ignore_ascii_case("openai") {
let default_auth = vtcode_auth::OpenAIAuthConfig::default();
let auth_cfg = ctx
.vt_cfg
.map(|cfg| &cfg.auth.openai)
.unwrap_or(&default_auth);
let storage_mode = ctx
.vt_cfg
.map(|cfg| cfg.agent.credential_storage_mode)
.unwrap_or_default();
let api_key = get_api_key("openai", &ApiKeySources::default()).ok();
let overview =
vtcode_config::auth::summarize_openai_credentials(auth_cfg, storage_mode, api_key)?;
let usage_status = if ctx.config.openai_chatgpt_auth.is_some() {
"using ChatGPT subscription"
} else {
match overview.active_source {
Some(vtcode_config::auth::OpenAIResolvedAuthSource::ChatGpt) => {
"using ChatGPT subscription"
}
Some(vtcode_config::auth::OpenAIResolvedAuthSource::ApiKey) => {
"using OPENAI_API_KEY"
}
None => "no active OpenAI credential",
}
};
renderer.line(
MessageStyle::Info,
&format!(
" Usage status: {} (preferred_method = {})",
usage_status,
overview.preferred_method.as_str()
),
)?;
if let Some(notice) = overview.notice.as_deref() {
renderer.line(MessageStyle::Info, &format!(" Notice: {}", notice))?;
}
if let Some(recommendation) = overview.recommendation.as_deref() {
renderer.line(
MessageStyle::Info,
&format!(" Recommendation: {}", recommendation),
)?;
}
} else if ctx.config.provider.eq_ignore_ascii_case("openrouter") {
let api_key = get_api_key("openrouter", &ApiKeySources::default()).ok();
let usage_status = match openrouter_status {
vtcode_auth::AuthStatus::Authenticated { .. } => "using OpenRouter OAuth",
vtcode_auth::AuthStatus::NotAuthenticated if api_key.is_some() => {
"using OPENROUTER_API_KEY"
}
vtcode_auth::AuthStatus::NotAuthenticated => "no active OpenRouter credential",
};
renderer.line(
MessageStyle::Info,
&format!(" Usage status: {}", usage_status),
)?;
} else if ctx.config.provider.eq_ignore_ascii_case("copilot") {
let usage_status = match copilot_status.kind {
CopilotAuthStatusKind::Authenticated => "using GitHub Copilot managed authentication",
CopilotAuthStatusKind::Unauthenticated => "no active GitHub Copilot credential",
CopilotAuthStatusKind::ServerUnavailable => "GitHub Copilot CLI unavailable",
CopilotAuthStatusKind::AuthFlowFailed => {
"GitHub Copilot authentication needs attention"
}
};
renderer.line(
MessageStyle::Info,
&format!(" Usage status: {}", usage_status),
)?;
if let Some(message) = copilot_status.message.as_deref()
&& !message.trim().is_empty()
{
renderer.line(MessageStyle::Info, &format!(" Notice: {}", message))?;
}
}
Ok(())
}
fn short_oauth_status_openrouter(status: &vtcode_auth::AuthStatus) -> &'static str {
match status {
vtcode_auth::AuthStatus::Authenticated { .. } => "connected",
vtcode_auth::AuthStatus::NotAuthenticated => "not connected",
}
}
fn short_copilot_status(status: &vtcode_core::copilot::CopilotAuthStatus) -> &'static str {
match status.kind {
CopilotAuthStatusKind::Authenticated => "connected",
CopilotAuthStatusKind::Unauthenticated => "not connected",
CopilotAuthStatusKind::ServerUnavailable => "CLI unavailable",
CopilotAuthStatusKind::AuthFlowFailed => "needs attention",
}
}
fn render_status_heading(renderer: &mut AnsiRenderer, heading: &str) -> Result<()> {
let style = MessageStyle::Info
.style()
.effects(MessageStyle::Info.style().get_effects() | Effects::BOLD);
renderer.line_with_override_style(MessageStyle::Info, style, heading)
}
fn current_session_id() -> String {
crate::main_helpers::runtime_archive_session_id()
.or_else(|| env::var("VT_SESSION_ID").ok())
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| "unavailable".to_string())
}
fn detect_ide_info() -> String {
let term_program = env::var("TERM_PROGRAM")
.ok()
.map(|value| value.to_ascii_lowercase());
if env::var("ZED_CLI").is_ok() || env::var("VIMRUNTIME").is_ok() {
return format_tool_version_label("Zed", detect_command_version("zed", &["--version"]));
}
if env::var("CURSOR_TRACE_ID").is_ok() || env::var("CURSOR_SESSION_ID").is_ok() {
return format_tool_version_label(
"Cursor",
detect_command_version("cursor", &["--version"]),
);
}
let in_vscode = env::var("VSCODE_PID").is_ok()
|| env::var("VSCODE_IPC_HOOK_CLI").is_ok()
|| term_program
.as_deref()
.is_some_and(|value| value.contains("vscode"));
if in_vscode {
return format_tool_version_label(
"VS Code",
detect_command_version("code", &["--version"]),
);
}
if env::var("JETBRAINS_IDE").is_ok() {
return "JetBrains (version unknown)".to_string();
}
"Not detected".to_string()
}
fn detect_terminal_info() -> String {
let terminal_type = TerminalType::detect().unwrap_or(TerminalType::Unknown);
let base_name = terminal_type.name();
let version = env::var("TERM_PROGRAM_VERSION")
.ok()
.filter(|value| !value.trim().is_empty())
.or_else(|| {
env::var("WEZTERM_VERSION")
.ok()
.filter(|value| !value.trim().is_empty())
});
if terminal_type == TerminalType::Unknown {
if let Ok(term) = env::var("TERM")
&& !term.trim().is_empty()
{
return format!("Unknown ({})", term.trim());
}
return "Unknown".to_string();
}
match version {
Some(version) => format!("{} {}", base_name, version.trim()),
None => format!("{} (version unknown)", base_name),
}
}
fn detect_command_version(command: &str, args: &[&str]) -> Option<String> {
let output = Command::new(command).args(args).output().ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let merged = if !stdout.trim().is_empty() {
stdout.trim()
} else {
stderr.trim()
};
let first_line = merged.lines().next()?.trim();
if first_line.is_empty() {
None
} else {
Some(first_line.to_string())
}
}
fn format_tool_version_label(name: &str, version: Option<String>) -> String {
match version {
Some(version) => format!("{} {}", name, version),
None => format!("{} (version unknown)", name),
}
}
async fn summarize_mcp_servers(
vt_cfg: Option<&VTCodeConfig>,
async_mcp_manager: Option<&AsyncMcpManager>,
) -> String {
let mut providers: Vec<String> = Vec::new();
if let Some(cfg) = vt_cfg {
if !cfg.mcp.enabled {
return "disabled".to_string();
}
for provider in cfg.mcp.providers.iter().filter(|provider| provider.enabled) {
let (transport_label, version_label) = match &provider.transport {
McpTransportConfig::Http(http) => {
("http", format!("protocol {}", http.protocol_version.trim()))
}
McpTransportConfig::Stdio(_) => ("stdio", "version unknown".to_string()),
};
providers.push(format!(
"{} ({}, {})",
provider.name.trim(),
transport_label,
version_label
));
}
}
let runtime_note = if let Some(manager) = async_mcp_manager {
match manager.get_status().await {
McpInitStatus::Ready { client } => {
let runtime = client.get_status();
if providers.is_empty() && !runtime.configured_providers.is_empty() {
providers.extend(
runtime
.configured_providers
.iter()
.map(|name| format!("{} (version unknown)", name.trim())),
);
}
Some(format!(
"active {}/{}",
runtime.active_connections, runtime.provider_count
))
}
McpInitStatus::Initializing { progress } => {
Some(format!("initializing: {}", progress.trim()))
}
McpInitStatus::Error { message } => Some(format!(
"error: {}",
truncate_status_value(message.trim(), 80)
)),
McpInitStatus::Disabled => Some("disabled".to_string()),
}
} else {
None
};
if providers.is_empty() {
providers.push("none configured".to_string());
}
match runtime_note {
Some(note) => format!("{} [{}]", providers.join(", "), note),
None => providers.join(", "),
}
}
async fn summarize_loaded_skills(loaded_skills: &Arc<RwLock<LoadedSkillsMap>>) -> String {
let skills = loaded_skills.read().await;
if skills.is_empty() {
return "none loaded".to_string();
}
let mut names: Vec<&str> = skills.keys().map(String::as_str).collect();
names.sort_unstable();
let visible_limit = 5;
if names.len() <= visible_limit {
return format!("{} loaded ({})", names.len(), names.join(", "));
}
let remaining = names.len() - visible_limit;
format!(
"{} loaded ({} +{} more)",
names.len(),
names[..visible_limit].join(", "),
remaining
)
}
fn summarize_project_memory(workspace: &Path) -> String {
let project_agents_path = workspace.join("AGENTS.md");
if project_agents_path.exists() {
"project (AGENTS.md)".to_string()
} else {
"project (AGENTS.md not found)".to_string()
}
}
async fn summarize_loaded_agents_sources(
vt_cfg: Option<&VTCodeConfig>,
active_instruction_directory: &Path,
instruction_context_paths: &[PathBuf],
) -> String {
let agent_config = vt_cfg.map(|cfg| cfg.agent.clone()).unwrap_or_default();
let appendix = load_instruction_appendix(
&agent_config,
active_instruction_directory,
instruction_context_paths,
)
.await;
let (agents, _) = instruction_memory_map(appendix.as_ref());
format_status_path_list(&agents)
}
pub(crate) fn instruction_memory_map(
appendix: Option<&vtcode_core::project_doc::InstructionAppendixBundle>,
) -> (Vec<String>, Vec<String>) {
let Some(bundle) = appendix else {
return (Vec::new(), Vec::new());
};
let Some(project_doc) = bundle.project_doc.as_ref() else {
return (Vec::new(), Vec::new());
};
let agents = project_doc
.segments
.iter()
.filter(|segment| matches!(segment.source.kind, InstructionSourceKind::Agents))
.map(|segment| {
format_instruction_path(
&segment.source.path,
bundle.project_root.as_path(),
bundle.home_dir.as_deref(),
)
})
.collect::<Vec<_>>();
let matched_rules = project_doc
.segments
.iter()
.filter(|segment| {
matches!(segment.source.kind, InstructionSourceKind::Rule) && segment.source.matched
})
.map(|segment| {
format_instruction_path(
&segment.source.path,
bundle.project_root.as_path(),
bundle.home_dir.as_deref(),
)
})
.collect::<Vec<_>>();
(agents, matched_rules)
}
fn format_status_path_list(paths: &[String]) -> String {
if paths.is_empty() {
"none".to_string()
} else {
paths.join(", ")
}
}
fn summarize_setting_sources(workspace: &Path) -> String {
let mut user_sources = Vec::new();
let mut project_sources = Vec::new();
if let Ok(manager) = ConfigManager::load_from_workspace(workspace) {
for layer in manager.layer_stack().layers() {
match &layer.source {
ConfigLayerSource::User { file } => {
user_sources.push(file.display().to_string());
}
ConfigLayerSource::Project { file } | ConfigLayerSource::Workspace { file } => {
project_sources.push(file.display().to_string());
}
ConfigLayerSource::System { .. } | ConfigLayerSource::Runtime => {}
}
}
}
let user_label = user_sources
.first()
.cloned()
.unwrap_or_else(|| "defaults".to_string());
let project_label = project_sources
.first()
.cloned()
.unwrap_or_else(|| workspace.join("vtcode.toml").display().to_string());
let vtcode_path = workspace.join(".vtcode");
let vtcode_label = if vtcode_path.exists() {
vtcode_path.display().to_string()
} else {
format!("{} (missing)", vtcode_path.display())
};
format!(
"user: {}, project: {}, .vtcode: {}",
user_label, project_label, vtcode_label
)
}
fn truncate_status_value(value: &str, max_len: usize) -> String {
if value.chars().count() <= max_len {
return value.to_string();
}
let mut truncated = String::new();
for ch in value.chars().take(max_len.saturating_sub(3)) {
truncated.push(ch);
}
truncated.push_str("...");
truncated
}
pub(crate) struct PlaceholderGuard {
handle: InlineHandle,
restore: Option<String>,
}
impl PlaceholderGuard {
pub(crate) fn new(handle: &InlineHandle, restore: Option<String>) -> Self {
Self {
handle: handle.clone(),
restore,
}
}
}
impl Drop for PlaceholderGuard {
fn drop(&mut self) {
self.handle.set_placeholder(self.restore.clone());
}
}
const SPINNER_UPDATE_INTERVAL_MS: u64 = 150;
async fn build_spinner_display(
current_message: &str,
progress_reporter: Option<&ProgressReporter>,
) -> String {
let progress_info = if let Some(progress_reporter) = progress_reporter {
let progress = progress_reporter.progress_info().await;
let mut parts = vec![progress.message.clone()];
if progress.total > 0 && progress.percentage > 0 {
parts.push(format!("{:.0}%", progress.percentage));
}
let eta = progress.eta_formatted();
if eta != "Calculating..." && eta != "0s" {
parts.push(eta);
}
parts.join(" ")
} else {
String::new()
};
if progress_info.is_empty() {
current_message.to_string()
} else {
format!("{}: {}", current_message, progress_info)
}
}
pub(crate) struct PlaceholderSpinner {
handle: InlineHandle,
restore_left: Option<String>,
restore_right: Option<String>,
active: Arc<AtomicBool>,
task: task::JoinHandle<()>,
message_sender: Option<watch::Sender<String>>,
defer_restore: Arc<AtomicBool>,
}
impl PlaceholderSpinner {
pub(crate) fn with_progress(
handle: &InlineHandle,
restore_left: Option<String>,
restore_right: Option<String>,
message: impl Into<String>,
progress_reporter: Option<&ProgressReporter>,
) -> Self {
let base_message = message.into();
let message_with_hint = with_stop_hint(&base_message);
let active = Arc::new(AtomicBool::new(true));
let spinner_active = active.clone();
let spinner_handle = handle.clone();
let restore_on_stop_left = restore_left.clone();
let restore_on_stop_right = restore_right.clone();
let status_right = restore_right.clone();
let progress_reporter = progress_reporter.cloned();
let (message_sender, mut message_receiver) = watch::channel(message_with_hint.clone());
let message_sender_clone = message_sender.clone();
let initial_display = message_with_hint.clone();
spinner_handle.set_input_status(Some(initial_display.clone()), status_right.clone());
let task = task::spawn(async move {
let mut current_message = message_with_hint;
let mut last_display = initial_display;
let mut interval =
tokio::time::interval(Duration::from_millis(SPINNER_UPDATE_INTERVAL_MS));
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
let mut message_updates_enabled = true;
while spinner_active.load(Ordering::SeqCst) {
tokio::select! {
_ = interval.tick() => {}
changed = message_receiver.changed(), if message_updates_enabled => {
match changed {
Ok(()) => {
current_message = message_receiver.borrow_and_update().clone();
}
Err(_) => {
message_updates_enabled = false;
continue;
}
}
}
}
let display =
build_spinner_display(¤t_message, progress_reporter.as_ref()).await;
if display != last_display {
spinner_handle.set_input_status(Some(display.clone()), status_right.clone());
last_display = display;
}
}
spinner_handle.set_input_status(restore_on_stop_left, restore_on_stop_right);
});
Self {
handle: handle.clone(),
restore_left,
restore_right,
active,
task,
message_sender: Some(message_sender_clone),
defer_restore: Arc::new(AtomicBool::new(false)),
}
}
pub(crate) fn new(
handle: &InlineHandle,
restore_left: Option<String>,
restore_right: Option<String>,
message: impl Into<String>,
) -> Self {
let mut spinner = Self::with_progress(handle, restore_left, restore_right, message, None);
spinner.message_sender = None;
spinner
}
pub(crate) fn set_defer_restore(&self, defer: bool) {
self.defer_restore.store(defer, Ordering::SeqCst);
}
pub(crate) fn update_message(&self, message: impl Into<String>) {
if let Some(sender) = &self.message_sender {
let _ = sender.send(with_stop_hint(&message.into()));
}
}
pub(crate) fn finish(&self) {
self.finish_with_restore(!self.defer_restore.load(Ordering::SeqCst));
}
pub(crate) fn set_reasoning_stage(&self, stage: Option<String>) {
self.handle.set_reasoning_stage(stage);
}
pub(crate) fn finish_with_restore(&self, restore: bool) {
if self.active.swap(false, Ordering::SeqCst) {
self.task.abort();
if restore {
self.handle
.set_input_status(self.restore_left.clone(), self.restore_right.clone());
}
}
}
}
impl Drop for PlaceholderSpinner {
fn drop(&mut self) {
self.finish();
self.task.abort();
}
}
pub(crate) fn start_loading_status(
handle: &InlineHandle,
input_status_state: &InputStatusState,
message: impl Into<String>,
) -> PlaceholderSpinner {
PlaceholderSpinner::new(
handle,
input_status_state.left.clone(),
input_status_state.right.clone(),
message,
)
}
#[derive(Default, Clone, Copy)]
pub(crate) struct StreamSpinnerOptions {
pub defer_finish: bool,
pub strip_proposed_plan_blocks: bool,
}
pub(crate) use vtcode_core::core::agent::runtime::RuntimeModelProgress as StreamProgressEvent;
#[cfg(test)]
pub(crate) async fn stream_and_render_response(
provider: &dyn uni::LLMProvider,
request: uni::LLMRequest,
spinner: &PlaceholderSpinner,
renderer: &mut AnsiRenderer,
ctrl_c_state: &Arc<CtrlCState>,
ctrl_c_notify: &Arc<Notify>,
) -> Result<(uni::LLMResponse, bool), uni::LLMError> {
stream_and_render_response_with_options(
provider,
request,
spinner,
renderer,
ctrl_c_state,
ctrl_c_notify,
StreamSpinnerOptions::default(),
)
.await
}
#[cfg(test)]
pub(crate) async fn stream_and_render_response_with_options(
provider: &dyn uni::LLMProvider,
request: uni::LLMRequest,
spinner: &PlaceholderSpinner,
renderer: &mut AnsiRenderer,
ctrl_c_state: &Arc<CtrlCState>,
ctrl_c_notify: &Arc<Notify>,
options: StreamSpinnerOptions,
) -> Result<(uni::LLMResponse, bool), uni::LLMError> {
stream_and_render_response_with_options_and_progress(
provider,
request,
spinner,
renderer,
ctrl_c_state,
ctrl_c_notify,
options,
None,
)
.await
}
pub(crate) async fn stream_and_render_response_with_options_and_progress(
provider: &dyn uni::LLMProvider,
request: uni::LLMRequest,
spinner: &PlaceholderSpinner,
renderer: &mut AnsiRenderer,
ctrl_c_state: &Arc<CtrlCState>,
ctrl_c_notify: &Arc<Notify>,
options: StreamSpinnerOptions,
on_progress: Option<&mut (dyn FnMut(StreamProgressEvent) + Send)>,
) -> Result<(uni::LLMResponse, bool), uni::LLMError> {
super::ui_interaction_stream::stream_and_render_response_with_options_impl(
provider,
request,
spinner,
renderer,
ctrl_c_state,
ctrl_c_notify,
options,
on_progress,
)
.await
}