Skip to main content

oino/modes/
interactive.rs

1//! Interactive mode: [`oino_tui::run`] over an in-process backend.
2
3use std::sync::Arc;
4
5use futures::StreamExt;
6use oino_client::{InProcessBackend, SessionBackend};
7use oino_core::events::{ClientEvent, OinoEvent};
8use oino_tui::TuiOptions;
9use oino_tui::host::LocalHost;
10
11use crate::modes::signals;
12use crate::startup::{Environment, Started};
13
14/// Runs the terminal UI until the user exits (0), an extension calls `ctx.shutdown()` and the
15/// session is idle (0), the session is taken over by another runner (0, with a notice), the
16/// UI fails (1) or a signal arrives.
17pub async fn run(started: Started, env: &Environment, options: TuiOptions) -> i32 {
18    let backend = Arc::new(InProcessBackend::new(started.runtime));
19    // The host asks the session for extension render specs (custom messages, entries and the
20    // built-in tool rows utility overrides; ui-protocol §6.4).
21    let host = Arc::new(
22        LocalHost::new(
23            &env.cwd,
24            &env.agent_dir,
25            Some(started.sessions_root),
26            started.project_trusted,
27        )
28        .with_render(Arc::clone(&backend) as Arc<dyn SessionBackend>),
29    );
30    let ui = oino_tui::run(
31        Arc::clone(&backend) as Arc<dyn SessionBackend>,
32        host,
33        options,
34    );
35    let shutdown = backend.shutdown_requested();
36    // D-TAKEOVER: when another runner takes the session over, this runtime closes it — show
37    // the notice and return to the session list (exit 0) instead of holding a dead session.
38    // Boxed: the stream keeps this mode's future small.
39    let takeover_notice = Box::pin(async {
40        let mut events = backend.events();
41        loop {
42            match events.next().await {
43                Some(ClientEvent::Oino(OinoEvent::SessionTakenOver { by })) => {
44                    return format!("Session moved to another window (by {})", by.client);
45                }
46                Some(_) => {}
47                None => std::future::pending().await,
48            }
49        }
50    });
51    let mut notice = None;
52    let code = tokio::select! {
53        () = shutdown => 0,
54        result = ui => match result {
55            Ok(()) => 0,
56            Err(error) => {
57                crate::eprintln_line(&format!("Error: {error}"));
58                1
59            }
60        },
61        taken = takeover_notice => {
62            notice = Some(taken);
63            0
64        }
65        signal = signals::next() => signal.exit_code(),
66    };
67    backend.shutdown().await;
68    if let Some(notice) = notice {
69        crate::eprintln_line(&notice);
70    }
71    code
72}