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