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::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
24pub 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum ShellSessionTransition {
33 FirstCall,
34 ModeChange,
35 Reset,
36 WorkspaceChange,
37}
38
39pub 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#[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}