chabeau 0.7.3

A full-screen terminal chat interface that connects to various AI APIs for real-time conversations
Documentation
use super::{App, AppActionContext, AppCommand};
use crate::core::message::AppMessageKind;

pub(super) fn handle_stream_error(
    app: &mut App,
    message: String,
    ctx: AppActionContext,
) -> Option<AppCommand> {
    let error_message = message.trim().to_string();
    if app.session.mcp_tools_enabled
        && !app.session.mcp_tools_unsupported
        && is_tool_unsupported_error(&error_message)
    {
        return handle_mcp_unsupported_error(app, ctx);
    }

    let input_area_height = app.input_area_height(ctx.term_width);
    {
        let mut conversation = app.conversation();
        conversation.remove_trailing_empty_assistant_messages();
        conversation.clear_pending_tool_calls();
        conversation.add_app_message(AppMessageKind::Error, error_message);
        let available_height =
            conversation.calculate_available_height(ctx.term_height, input_area_height);
        conversation.update_scroll_position(available_height, ctx.term_width);
    }
    app.end_streaming();
    None
}

fn handle_mcp_unsupported_error(app: &mut App, ctx: AppActionContext) -> Option<AppCommand> {
    app.session.mcp_tools_unsupported = true;
    app.session.mcp_tools_enabled = false;

    let input_area_height = app.input_area_height(ctx.term_width);
    {
        let mut conversation = app.conversation();
        conversation.remove_trailing_empty_assistant_messages();
        conversation.clear_pending_tool_calls();
        conversation.add_app_message(
            AppMessageKind::Warning,
            "MCP tool-calling is enabled, but the currently selected model does not support it. It will be disabled until you switch models."
                .to_string(),
        );
        let available_height =
            conversation.calculate_available_height(ctx.term_height, input_area_height);
        conversation.update_scroll_position(available_height, ctx.term_width);
    }
    app.end_streaming();

    let base_messages = app
        .session
        .tool_pipeline
        .continuation_messages
        .as_ref()
        .map(|continuation| continuation.api_messages_base.clone())?;

    if ctx.term_width == 0 || ctx.term_height == 0 {
        return None;
    }

    app.ui.focus_transcript();
    app.enable_auto_scroll();

    let input_area_height = app.input_area_height(ctx.term_width);
    let (cancel_token, stream_id) = {
        let mut conversation = app.conversation();
        let (cancel_token, stream_id) = conversation.start_new_stream();
        conversation.add_assistant_placeholder();
        let available_height =
            conversation.calculate_available_height(ctx.term_height, input_area_height);
        conversation.update_scroll_position(available_height, ctx.term_width);
        (cancel_token, stream_id)
    };

    Some(AppCommand::SpawnStream(app.build_stream_params(
        base_messages,
        cancel_token,
        stream_id,
    )))
}

fn is_tool_unsupported_error(message: &str) -> bool {
    let lower = message.to_ascii_lowercase();
    let mentions_tools = lower.contains("tools")
        || lower.contains("tool_calls")
        || lower.contains("tool call")
        || lower.contains("function_call")
        || lower.contains("function calling");
    if !mentions_tools {
        return false;
    }

    let unsupported_signals = [
        "not supported",
        "unsupported",
        "unknown field",
        "unknown parameter",
        "unrecognized",
        "unexpected field",
        "invalid parameter",
        "extra fields",
        "does not support",
    ];

    unsupported_signals
        .iter()
        .any(|signal| lower.contains(signal))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::app::actions::AppCommand;
    use crate::core::message::TranscriptRole;
    use crate::utils::test_utils::create_test_app;

    fn default_ctx() -> AppActionContext {
        AppActionContext {
            term_width: 80,
            term_height: 24,
        }
    }

    #[test]
    fn stream_error_trims_message_and_keeps_user_history() {
        let mut app = create_test_app();
        let ctx = default_ctx();

        let command = super::super::stream_lifecycle::spawn_stream_for_message(
            &mut app,
            "Hello there".into(),
            ctx,
        );

        assert!(matches!(command, Some(AppCommand::SpawnStream(_))));

        let result = handle_stream_error(&mut app, " network failure ".into(), ctx);
        assert!(result.is_none());
        assert!(app
            .ui
            .messages
            .iter()
            .all(|m| m.role != TranscriptRole::Assistant || !m.content.trim().is_empty()));
        let last = app.ui.messages.back().expect("last");
        assert_eq!(last.role, TranscriptRole::AppError);
        assert_eq!(last.content, "network failure");
        assert_eq!(
            app.ui.messages.front().expect("first").role,
            TranscriptRole::User
        );
    }
}