Skip to main content

winx_code_agent/runtime/
mod.rs

1//! Shell execution boundary shared by embedded and daemon-backed runtimes.
2
3mod selection;
4mod session_store;
5
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::Arc;
9
10use tokio::sync::Mutex;
11
12use crate::errors::Result;
13use crate::state::bash_state::BashState;
14use crate::types::BashCommand;
15
16#[cfg(unix)]
17pub use selection::ensure_daemon_at;
18pub use selection::{
19    configured_runtime_mode, configured_shell_runtime, select_runtime_mode, RuntimeMode,
20};
21pub(crate) use session_store::lock_session_store;
22pub use session_store::{SessionStore, ShellTarget};
23
24/// Boxed future returned by [`ShellRuntime`] implementations.
25pub type ShellRuntimeFuture<'a> = Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>>;
26pub type ShellRuntimeUnitFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
27pub type ShellRuntimeConfigureFuture<'a> =
28    Pin<Box<dyn Future<Output = Result<Option<String>>> + Send + 'a>>;
29
30/// State transition requested by the Initialize tool.
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum ShellSessionTransition {
33    FirstCall,
34    ModeChange,
35    Reset,
36    WorkspaceChange,
37}
38
39/// Narrow process-agnostic boundary for BashCommand execution.
40pub trait ShellRuntime: Send + Sync {
41    fn configure_session<'a>(
42        &'a self,
43        bash_state: &'a mut BashState,
44        transition: ShellSessionTransition,
45    ) -> ShellRuntimeConfigureFuture<'a>;
46
47    fn run_action<'a>(
48        &'a self,
49        bash_state: &'a Arc<Mutex<Option<BashState>>>,
50        command: BashCommand,
51    ) -> ShellRuntimeFuture<'a>;
52
53    fn interrupt<'a>(
54        &'a self,
55        bash_state: &'a Arc<Mutex<Option<BashState>>>,
56    ) -> ShellRuntimeUnitFuture<'a>;
57}
58
59/// Current in-process runtime. It intentionally delegates to the existing
60/// implementation so introducing the boundary does not change behavior.
61#[derive(Clone, Copy, Debug, Default)]
62pub struct EmbeddedShellRuntime;
63
64impl ShellRuntime for EmbeddedShellRuntime {
65    fn configure_session<'a>(
66        &'a self,
67        bash_state: &'a mut BashState,
68        transition: ShellSessionTransition,
69    ) -> ShellRuntimeConfigureFuture<'a> {
70        Box::pin(async move {
71            if matches!(
72                transition,
73                ShellSessionTransition::FirstCall | ShellSessionTransition::Reset
74            ) {
75                if transition == ShellSessionTransition::Reset || bash_state.cwd.exists() {
76                    bash_state.init_pty_shell().await?;
77                }
78            }
79            let attach_hint = {
80                let shell = bash_state.pty_shell.lock().await;
81                shell.as_ref().and_then(|shell| shell.attach_hint.clone())
82            };
83            Ok(attach_hint)
84        })
85    }
86
87    fn run_action<'a>(
88        &'a self,
89        bash_state: &'a Arc<Mutex<Option<BashState>>>,
90        command: BashCommand,
91    ) -> ShellRuntimeFuture<'a> {
92        Box::pin(crate::tools::bash_command::handle_embedded_tool_call(bash_state, command))
93    }
94
95    fn interrupt<'a>(
96        &'a self,
97        bash_state: &'a Arc<Mutex<Option<BashState>>>,
98    ) -> ShellRuntimeUnitFuture<'a> {
99        Box::pin(interrupt_embedded(bash_state))
100    }
101}
102
103async fn interrupt_embedded(bash_state: &Arc<Mutex<Option<BashState>>>) -> Result<()> {
104    let shell = {
105        let state = bash_state.lock().await;
106        state.as_ref().map(|state| state.pty_shell.clone())
107    };
108    let Some(shell) = shell else { return Ok(()) };
109    {
110        let mut guard = shell.lock().await;
111        if let Some(pty) = guard.as_mut() {
112            pty.send_interrupt().map_err(|error| {
113                crate::errors::WinxError::CommandExecutionError(format!(
114                    "failed to interrupt shell: {error}"
115                ))
116            })?;
117        }
118    }
119
120    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
121    while std::time::Instant::now() < deadline {
122        let recovered = {
123            let mut guard = shell.lock().await;
124            match guard.as_mut() {
125                Some(pty) => pty.poll_output_nonblocking() || !pty.command_running,
126                None => true,
127            }
128        };
129        if recovered {
130            return Ok(());
131        }
132        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
133    }
134    Err(crate::errors::WinxError::CommandExecutionError(
135        "interrupted shell did not return to a prompt within 3 seconds".to_string(),
136    ))
137}