Skip to main content

mobius_cli/frontend/
setup.rs

1//! Gateway-native provider login and Bot setup wizard.
2
3mod runtime;
4mod state;
5pub(in crate::frontend) mod view;
6
7use std::io;
8
9use mobius::{Error, Result};
10use mobius_gateway::client::{GatewayEvents, GatewaySender};
11use mobius_gateway::config::{
12    MAX_PROVIDER_API_KEY_BYTES as MAX_API_KEY_BYTES, MAX_PROVIDER_LABEL_BYTES,
13};
14use mobius_gateway::wire::{ReadyPayload, SessionReadyPayload};
15use ratatui::Terminal;
16use ratatui::backend::CrosstermBackend;
17
18use self::runtime::{apply, apply_gateway, edit};
19use self::state::SetupState;
20use super::terminal::TerminalGuard;
21
22const MAX_ENDPOINT_BYTES: usize = 4 * 1024;
23const MAX_MODEL_IDS_BYTES: usize = 16 * 1024;
24const MIN_INLINE_DESCRIPTION_WIDTH: usize = 20;
25
26const UPDATE_BOT_LABEL: &str = "Update this Bot";
27const UPDATE_BOT_DESCRIPTION: &str = "Apply these settings to every chat owned by this Bot";
28const SAVE_DEFAULT_LABEL: &str = "Save Bot template";
29const SAVE_DEFAULT_DESCRIPTION: &str = "Use these settings when creating Bots";
30
31pub(in crate::frontend) type SetupTerminal = Terminal<CrosstermBackend<io::Stdout>>;
32
33/// The focused setup flow requested by the CLI shell.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub(crate) enum SetupMode {
36    Login,
37    Bot,
38    BotModel,
39}
40
41/// Runs one gateway-backed setup flow and updates its machine and chat snapshots.
42pub(crate) async fn run(
43    terminal: &mut SetupTerminal,
44    mode: SetupMode,
45    preferred_provider: Option<&str>,
46    sender: &GatewaySender,
47    events: &mut GatewayEvents,
48    gateway: &mut ReadyPayload,
49    session: &mut SessionReadyPayload,
50) -> Result<()> {
51    run_bot(
52        terminal,
53        mode,
54        preferred_provider,
55        sender,
56        events,
57        gateway,
58        &session.session.context.owner_id,
59    )
60    .await
61}
62
63/// Edits one durable Bot without requiring an open chat.
64pub(in crate::frontend) async fn run_bot(
65    terminal: &mut SetupTerminal,
66    mode: SetupMode,
67    preferred_provider: Option<&str>,
68    sender: &GatewaySender,
69    events: &mut GatewayEvents,
70    gateway: &mut ReadyPayload,
71    bot_id: &str,
72) -> Result<()> {
73    let bot = gateway
74        .bots
75        .iter()
76        .find(|bot| bot.id == bot_id)
77        .ok_or_else(|| Error::Config("the selected Bot is not in the gateway catalog".into()))?;
78    let mut state = SetupState::new(
79        mode,
80        preferred_provider,
81        gateway,
82        bot.config.config.clone(),
83        false,
84    )?;
85    terminal.clear()?;
86
87    while edit(terminal, &mut state, sender, events, gateway).await? {
88        match apply(terminal, &mut state, sender, events, gateway, bot_id).await {
89            Ok(()) => return Ok(()),
90            Err(error) => state.recover_error(error)?,
91        }
92    }
93    Ok(())
94}
95
96/// Runs provider or Bot-template setup without creating or changing a chat.
97pub(crate) async fn run_gateway(
98    terminal: &mut SetupTerminal,
99    mode: SetupMode,
100    preferred_provider: Option<&str>,
101    sender: &GatewaySender,
102    events: &mut GatewayEvents,
103    gateway: &mut ReadyPayload,
104) -> Result<()> {
105    let original = gateway
106        .bot_defaults
107        .as_ref()
108        .map(|default| default.config.clone())
109        .unwrap_or_default();
110    if mode == SetupMode::Bot && gateway.bot_defaults.is_none() {
111        return Err(Error::Config(
112            "configure a provider before changing the Bot template".into(),
113        ));
114    }
115    let mut state = SetupState::new(mode, preferred_provider, gateway, original, true)?;
116    terminal.clear()?;
117    while edit(terminal, &mut state, sender, events, gateway).await? {
118        match apply_gateway(terminal, &mut state, sender, events, gateway).await {
119            Ok(()) => return Ok(()),
120            Err(error) => state.recover_error(error)?,
121        }
122    }
123    Ok(())
124}
125
126/// Runs gateway-scoped provider setup before any chat exists.
127/// Returns `false` when setup is cancelled.
128/// # Errors
129///
130/// Returns an error if validation or an operation required to complete the request fails.
131pub async fn run_gateway_login(
132    sender: &GatewaySender,
133    events: &mut GatewayEvents,
134    gateway: &mut ReadyPayload,
135) -> Result<bool> {
136    let mut guard = TerminalGuard::alternate()?;
137    guard.set_mouse_capture(false)?;
138    let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
139    let mut state = SetupState::new(
140        SetupMode::Login,
141        None,
142        gateway,
143        gateway
144            .bot_defaults
145            .as_ref()
146            .map(|defaults| defaults.config.clone())
147            .unwrap_or_default(),
148        true,
149    )?;
150    state.start_chat = true;
151    terminal.clear()?;
152    while edit(&mut terminal, &mut state, sender, events, gateway).await? {
153        match apply_gateway(&mut terminal, &mut state, sender, events, gateway).await {
154            Ok(()) => return Ok(true),
155            Err(error) => state.recover_error(error)?,
156        }
157    }
158    Ok(false)
159}
160
161#[cfg(test)]
162mod tests;