codewhale-tui 0.9.8

Terminal UI for open-source and open-weight coding models
Documentation
//! Welcome screen content for onboarding.

use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};

use crate::localization::MessageId;
use crate::palette;
use crate::tui::app::App;

pub fn lines(app: &App) -> Vec<Line<'static>> {
    let steps = welcome_step_labels(app).join(" -> ");
    let version = app
        .tr(MessageId::OnboardWelcomeVersion)
        .replace("{version}", env!("CARGO_PKG_VERSION"));
    let next_steps = app
        .tr(MessageId::OnboardWelcomeSteps)
        .replace("{steps}", &steps);

    vec![
        Line::from(Span::styled(
            "codewhale",
            Style::default()
                .fg(palette::WHALE_HUMAN)
                .add_modifier(Modifier::BOLD),
        )),
        Line::from(Span::styled(
            version,
            Style::default().fg(palette::TEXT_MUTED),
        )),
        Line::from(""),
        Line::from(Span::styled(
            app.tr(MessageId::OnboardWelcomeLead).to_string(),
            Style::default().fg(palette::TEXT_PRIMARY),
        )),
        Line::from(Span::styled(
            app.tr(MessageId::OnboardWelcomeSetupBlurb).to_string(),
            Style::default().fg(palette::TEXT_MUTED),
        )),
        Line::from(Span::styled(
            next_steps,
            Style::default().fg(palette::TEXT_MUTED),
        )),
        Line::from(Span::styled(
            app.tr(MessageId::OnboardWelcomeDefaults).to_string(),
            Style::default().fg(palette::TEXT_MUTED),
        )),
        Line::from(""),
        Line::from(Span::styled(
            app.tr(MessageId::OnboardWelcomeEnter).to_string(),
            Style::default().fg(palette::TEXT_PRIMARY),
        )),
        Line::from(Span::styled(
            app.tr(MessageId::OnboardWelcomeExit).to_string(),
            Style::default().fg(palette::TEXT_MUTED),
        )),
    ]
}

fn welcome_step_labels(app: &App) -> Vec<String> {
    let mut steps = vec![
        app.tr(MessageId::OnboardWelcomeStepLanguage).to_string(),
        // #3937: the appearance step is unconditional, so the preview names it.
        app.tr(MessageId::OnboardWelcomeStepAppearance).to_string(),
    ];
    if app.onboarding_needs_api_key {
        steps.push(app.tr(MessageId::OnboardWelcomeStepApiKey).to_string());
    }
    if !app.trust_mode && super::needs_trust(&app.workspace) {
        steps.push(app.tr(MessageId::OnboardWelcomeStepTrust).to_string());
    }
    steps.push(
        app.tr(MessageId::OnboardWelcomeStepMentalModels)
            .to_string(),
    );
    steps.push(app.tr(MessageId::OnboardWelcomeStepTips).to_string());
    steps
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::Config;
    use crate::localization::Locale;
    use crate::tui::app::TuiOptions;
    use std::path::PathBuf;

    fn test_app_with_locale(locale: Locale) -> App {
        let options = TuiOptions {
            ..crate::test_support::test_tui_options(PathBuf::from("."))
        };
        let mut app = App::new(options, &Config::default());
        app.ui_locale = locale;
        app
    }

    fn body(app: &App) -> String {
        lines(app)
            .into_iter()
            .flat_map(|line| {
                line.spans
                    .into_iter()
                    .map(|span| span.content.to_string())
                    .collect::<Vec<_>>()
            })
            .collect::<Vec<_>>()
            .join("\n")
    }

    #[test]
    fn welcome_copy_centers_constitution_first_setup() {
        let mut app = test_app_with_locale(Locale::En);
        app.onboarding_needs_api_key = false;
        app.trust_mode = true;
        let body = body(&app);

        // The dual meaning of "code" opens the arc: software and law.
        assert!(body.contains("Code means two things"));
        assert!(body.contains("the law this agent works under"));
        assert!(body.contains("only these screens will appear"));
        assert!(body.contains(
            "Next: choose language -> pick a look -> learn modes and permissions -> setup tips."
        ));
        assert!(body.contains("/constitution"));
        assert!(!body.contains("add an API key"));
        assert!(!body.contains("land in the chat"));
    }

    #[test]
    fn welcome_wordmark_uses_the_human_brand_lane() {
        let app = test_app_with_locale(Locale::En);
        let welcome = lines(&app);
        assert_eq!(welcome[0].spans[0].style.fg, Some(palette::WHALE_HUMAN));
        assert_ne!(palette::WHALE_HUMAN, palette::WHALE_ACTION);
    }

    #[test]
    fn welcome_steps_include_optional_api_key_and_trust_screens() {
        let tmp = tempfile::tempdir().expect("tempdir");
        let mut app = test_app_with_locale(Locale::En);
        app.workspace = tmp.path().to_path_buf();
        app.onboarding_needs_api_key = true;
        app.trust_mode = false;

        let body = body(&app);

        assert!(body.contains(
            "Next: choose language -> pick a look -> connect API key -> trust workspace -> learn modes and permissions -> setup tips."
        ));
    }

    #[test]
    fn welcome_copy_uses_locale_registry() {
        let mut app = test_app_with_locale(Locale::ZhHans);
        app.onboarding_needs_api_key = false;
        app.trust_mode = true;

        let body = body(&app);

        assert!(body.contains("Codewhale 会与你协作完成工作"));
        assert!(!body.contains("代码在这里有两层含义"));
        assert!(body.contains("接下来:选择语言 -> 选择外观 -> 了解模式与权限 -> 设置提示。"));
        assert!(!body.contains("Press Enter"));
    }
}