winx_code_agent/runtime/
mod.rs1mod 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
26pub 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub enum ShellSessionTransition {
42 FirstCall,
43 ModeChange,
44 Reset,
45 WorkspaceChange,
46}
47
48pub 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 fn terminate_session<'a>(&'a self, thread_id: &'a str) -> ShellRuntimeUnitFuture<'a>;
69}
70
71#[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}