1use anyhow::Context as AnyhowContext;
8use rand::RngExt;
9use std::collections::HashMap;
10use std::fmt::Write as FmtWrite;
11use std::io::Write;
12use std::path::Path;
13use std::process::{Command, Stdio};
14use std::sync::{Arc, Mutex as StdMutex};
15use std::time::{Duration, Instant};
16use tokio::sync::Mutex;
17use tokio::time::sleep;
18use tracing::{debug, error, info, warn};
19
20use crate::errors::{Result, WinxError};
21use crate::state::bash_state::{BashState, CommandState, InteractiveBash};
22use crate::state::terminal::{render_terminal_output, strip_ansi_codes};
23use crate::types::{normalize_thread_id, BashCommand, BashCommandAction, SpecialKey};
24
25const DEFAULT_TIMEOUT: f64 = 5.0;
29
30const TIMEOUT_WHILE_OUTPUT: f64 = 20.0;
32
33const OUTPUT_WAIT_PATIENCE: i32 = 3;
35
36const COMMAND_CHUNK_SIZE: usize = 64;
38
39const TEXT_CHUNK_SIZE: usize = 128;
41
42const MAX_OUTPUT_LENGTH: usize = 100_000;
44
45const WAITING_INPUT_MESSAGE: &str = "A command is already running. NOTE: You can't run multiple shell commands in main shell, likely a previous program hasn't exited.
471. Get its output using status check.
482. Use `send_ascii` or `send_specials` to give inputs to the running program OR
493. kill the previous program by sending ctrl+c first using `send_ascii` or `send_specials`
504. Interrupt and run the process in background
51";
52
53#[derive(Debug, Default)]
57pub struct BackgroundShellManager {
58 shells: HashMap<String, Arc<StdMutex<Option<InteractiveBash>>>>,
59}
60
61impl BackgroundShellManager {
62 pub fn new() -> Self {
64 Self { shells: HashMap::new() }
65 }
66
67 pub fn start_new_shell(&mut self, working_dir: &Path, restricted_mode: bool) -> Result<String> {
69 let cid = format!("{:010x}", rand::rng().random::<u32>());
70
71 let bash = InteractiveBash::new(working_dir, restricted_mode).map_err(|e| {
72 WinxError::CommandExecutionError(format!("Failed to start background shell: {e}"))
73 })?;
74
75 self.shells.insert(cid.clone(), Arc::new(StdMutex::new(Some(bash))));
76
77 info!("Started background shell with id: {}", cid);
78 Ok(cid)
79 }
80
81 pub fn get_shell(&self, bg_command_id: &str) -> Option<Arc<StdMutex<Option<InteractiveBash>>>> {
83 self.shells.get(bg_command_id).cloned()
84 }
85
86 pub fn remove_shell(&mut self, bg_command_id: &str) -> bool {
88 if let Some(shell_arc) = self.shells.remove(bg_command_id) {
89 if let Ok(mut guard) = shell_arc.lock() {
90 *guard = None; }
92 info!("Removed background shell: {}", bg_command_id);
93 true
94 } else {
95 false
96 }
97 }
98
99 pub fn get_running_info(&self) -> String {
101 if self.shells.is_empty() {
102 return "No command running in background.\n".to_string();
103 }
104
105 let mut running = Vec::new();
106 for (id, shell_arc) in &self.shells {
107 if let Ok(guard) = shell_arc.lock() {
108 if let Some(bash) = guard.as_ref() {
109 running.push(format!("Command: {}, bg_command_id: {}", bash.last_command, id));
110 }
111 }
112 }
113
114 if running.is_empty() {
115 "No command running in background.\n".to_string()
116 } else {
117 format!("Following background commands are attached:\n{}\n", running.join("\n"))
118 }
119 }
120}
121
122lazy_static::lazy_static! {
124 static ref BG_SHELL_MANAGER: StdMutex<BackgroundShellManager> = StdMutex::new(BackgroundShellManager::new());
125}
126
127fn get_status(
131 bash_state: &BashState,
132 is_bg: bool,
133 bg_id: Option<&str>,
134 is_running: bool,
135 running_for: Option<&str>,
136) -> String {
137 let mut status = "\n\n---\n\n".to_string();
138
139 if is_bg {
140 if let Some(id) = bg_id {
141 let _ = writeln!(status, "bg_command_id = {id}");
142 }
143 }
144
145 if is_running {
146 status.push_str("status = still running\n");
147 if let Some(duration) = running_for {
148 let _ = writeln!(status, "running for = {duration}");
149 }
150 } else {
151 status.push_str("status = process exited\n");
152 }
153
154 let _ = writeln!(status, "cwd = {}", bash_state.cwd.display());
155
156 if !is_bg {
157 if let Ok(manager) = BG_SHELL_MANAGER.lock() {
159 status.push_str("This is the main shell. ");
160 status.push_str(&manager.get_running_info());
161 }
162 }
163
164 status.trim_end().to_string()
165}
166
167fn wcgw_incremental_text(text: &str, last_pending_output: &str) -> String {
169 let text =
170 if text.len() > MAX_OUTPUT_LENGTH { &text[text.len() - MAX_OUTPUT_LENGTH..] } else { text };
171
172 if last_pending_output.is_empty() {
173 let rendered = render_terminal_output(text);
174 return rstrip_lines(&rendered).trim_start().to_string();
175 }
176
177 let last_rendered = render_terminal_output(last_pending_output);
178 if last_rendered.is_empty() {
179 return rstrip_lines(&render_terminal_output(text));
180 }
181
182 let text_after_last = if text.len() > last_pending_output.len() {
184 &text[last_pending_output.len()..]
185 } else {
186 text
187 };
188
189 let combined = format!("{}\n{}", last_rendered.join("\n"), text_after_last);
190 let new_rendered = render_terminal_output(&combined);
191
192 let incremental = get_incremental_output(&last_rendered, &new_rendered);
194 rstrip_lines(&incremental)
195}
196
197fn rstrip_lines(lines: &[String]) -> String {
199 lines.iter().map(|line| line.trim_end()).collect::<Vec<_>>().join("\n")
200}
201
202fn get_incremental_output(old_output: &[String], new_output: &[String]) -> Vec<String> {
204 if old_output.is_empty() {
205 return new_output.to_vec();
206 }
207
208 let nold = old_output.len();
209 let nnew = new_output.len();
210
211 for i in (0..nnew).rev() {
213 if new_output[i] != old_output[nold - 1] {
214 continue;
215 }
216
217 let mut matched = true;
218 for j in (0..i).rev() {
219 let old_idx = (nold as i64 - 1 + j as i64 - i as i64) as isize;
220 if old_idx < 0 {
221 break;
222 }
223 if new_output[j] != old_output[old_idx as usize] {
224 matched = false;
225 break;
226 }
227 }
228
229 if matched {
230 return new_output[i + 1..].to_vec();
231 }
232 }
233
234 new_output.to_vec()
235}
236
237#[allow(dead_code)]
239fn is_status_check_action(action: &BashCommandAction) -> bool {
240 match action {
241 BashCommandAction::StatusCheck { .. } => true,
242 BashCommandAction::SendSpecials { send_specials, .. } => {
243 send_specials.len() == 1 && send_specials[0] == SpecialKey::Enter
244 }
245 BashCommandAction::SendAscii { send_ascii, .. } => {
246 send_ascii.len() == 1 && send_ascii[0] == 10 }
248 _ => false,
249 }
250}
251
252#[tracing::instrument(level = "info", skip(bash_state_arc, bash_command))]
259pub async fn handle_tool_call(
260 bash_state_arc: &Arc<Mutex<Option<BashState>>>,
261 bash_command: BashCommand,
262) -> Result<String> {
263 info!("BashCommand tool called with: {:?}", bash_command);
264
265 let thread_id = normalize_thread_id(&bash_command.thread_id);
266
267 if thread_id.is_empty() {
269 error!("Empty thread_id provided in BashCommand");
270 return Err(WinxError::ThreadIdMismatch(
271 "Error: No saved bash state found for thread ID \"\". Please initialize first with this ID.".to_string()
272 ));
273 }
274
275 let mut bash_state: BashState;
277 {
278 let bash_state_guard = bash_state_arc.lock().await;
279
280 let Some(state) = &*bash_state_guard else {
281 error!("BashState not initialized");
282 return Err(WinxError::BashStateNotInitialized);
283 };
284
285 bash_state = state.clone();
286 }
287
288 if thread_id != bash_state.current_thread_id {
290 if !bash_state.load_state_from_disk(&thread_id).unwrap_or(false) {
292 return Err(WinxError::ThreadIdMismatch(format!(
293 "Error: No saved bash state found for thread_id `{thread_id}`. Please initialize first with this ID."
294 )));
295 }
296 }
297
298 let timeout_s = bash_command
301 .wait_for_seconds
302 .map_or(DEFAULT_TIMEOUT, |t| f64::from(t).max(0.0))
303 .min(TIMEOUT_WHILE_OUTPUT);
304
305 let result = execute_bash_action(&mut bash_state, &bash_command.action_json, timeout_s).await;
307
308 match result {
310 Ok(mut output) => {
311 if let BashCommandAction::Command { ref command, .. } = bash_command.action_json {
312 let cmd_trimmed = command.trim();
313 if output.starts_with(cmd_trimmed) {
314 output = output[cmd_trimmed.len()..].to_string();
315 }
316 }
317 Ok(output)
318 }
319 Err(e) => Err(e),
320 }
321}
322
323async fn execute_bash_action(
325 bash_state: &mut BashState,
326 action: &BashCommandAction,
327 timeout_s: f64,
328) -> Result<String> {
329 let mut is_bg = false;
330 let mut bg_id: Option<String> = None;
331
332 let bg_shell: Option<Arc<StdMutex<Option<InteractiveBash>>>> = match action {
334 BashCommandAction::Command { .. } => None, BashCommandAction::StatusCheck { bg_command_id, .. }
336 | BashCommandAction::SendText { bg_command_id, .. }
337 | BashCommandAction::SendSpecials { bg_command_id, .. }
338 | BashCommandAction::SendAscii { bg_command_id, .. } => {
339 if let Some(id) = bg_command_id {
340 let manager = BG_SHELL_MANAGER.lock().map_err(|e| {
341 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
342 })?;
343
344 if let Some(shell) = manager.get_shell(id) {
345 is_bg = true;
346 bg_id = Some(id.clone());
347 Some(shell)
348 } else {
349 let error = format!(
351 "No shell found running with command id {}.\n{}",
352 id,
353 manager.get_running_info()
354 );
355 return Err(WinxError::CommandExecutionError(error));
356 }
357 } else {
358 None
359 }
360 }
361 };
362
363 match action {
365 BashCommandAction::Command { command, is_background } => {
366 execute_command(bash_state, command, *is_background, timeout_s).await
367 }
368 BashCommandAction::StatusCheck { .. } => {
369 execute_status_check(bash_state, bg_shell, is_bg, bg_id.as_deref(), timeout_s).await
370 }
371 BashCommandAction::SendText { send_text, .. } => {
372 execute_send_text(bash_state, send_text, bg_shell, is_bg, bg_id.as_deref(), timeout_s)
373 .await
374 }
375 BashCommandAction::SendSpecials { send_specials, .. } => {
376 execute_send_specials(
377 bash_state,
378 send_specials,
379 bg_shell,
380 is_bg,
381 bg_id.as_deref(),
382 timeout_s,
383 )
384 .await
385 }
386 BashCommandAction::SendAscii { send_ascii, .. } => {
387 execute_send_ascii(bash_state, send_ascii, bg_shell, is_bg, bg_id.as_deref(), timeout_s)
388 .await
389 }
390 }
391}
392
393async fn execute_command(
395 bash_state: &mut BashState,
396 command: &str,
397 is_background: bool,
398 timeout_s: f64,
399) -> Result<String> {
400 debug!("Processing Command action: {}", command);
401
402 if !bash_state.is_command_allowed(command) {
404 error!("Command '{}' not allowed in current mode", command);
405 return Err(WinxError::CommandNotAllowed(
406 "Error: BashCommand not allowed in current mode".to_string(),
407 ));
408 }
409
410 let command = command.trim();
412 crate::utils::bash_parser::assert_single_statement(command)?;
413
414 if is_background {
416 return execute_in_background(bash_state, command, timeout_s).await;
417 }
418
419 {
421 let bash_guard = bash_state.interactive_bash.lock().map_err(|e| {
422 WinxError::BashStateLockError(format!("Failed to lock bash state: {e}"))
423 })?;
424
425 if let Some(ref bash) = *bash_guard {
426 if let CommandState::Running { .. } = bash.command_state {
427 return Err(WinxError::CommandExecutionError(WAITING_INPUT_MESSAGE.to_string()));
428 }
429 }
430 }
431
432 if bash_state
434 .interactive_bash
435 .lock()
436 .map_err(|e| WinxError::BashStateLockError(format!("Failed to lock bash state: {e}")))?
437 .is_none()
438 {
439 bash_state
440 .init_interactive_bash()
441 .map_err(|e| WinxError::CommandExecutionError(format!("Failed to init bash: {e}")))?;
442 }
443
444 {
449 let mut bash_guard = bash_state.interactive_bash.lock().map_err(|e| {
450 WinxError::BashStateLockError(format!("Failed to lock bash state: {e}"))
451 })?;
452
453 let bash = bash_guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
454
455 for chunk in command.as_bytes().chunks(COMMAND_CHUNK_SIZE) {
457 if let Some(mut stdin) = bash.process.stdin.take() {
458 stdin.write_all(chunk).map_err(|e| {
459 WinxError::CommandExecutionError(format!("Failed to write chunk: {e}"))
460 })?;
461 bash.process.stdin = Some(stdin);
462 }
463 }
464
465 if let Some(mut stdin) = bash.process.stdin.take() {
467 stdin.write_all(b"\n").map_err(|e| {
468 WinxError::CommandExecutionError(format!("Failed to write newline: {e}"))
469 })?;
470 stdin
471 .flush()
472 .map_err(|e| WinxError::CommandExecutionError(format!("Failed to flush: {e}")))?;
473 bash.process.stdin = Some(stdin);
474 }
475
476 bash.last_command = command.to_string();
477 bash.command_state = CommandState::Running {
478 start_time: std::time::SystemTime::now(),
479 command: command.to_string(),
480 };
481 }
482
483 wait_for_output(bash_state, timeout_s, false, None, false).await
485}
486
487async fn wait_for_output(
489 bash_state: &mut BashState,
490 timeout_s: f64,
491 is_bg: bool,
492 bg_id: Option<&str>,
493 is_status_check: bool,
494) -> Result<String> {
495 let start = Instant::now();
496 let wait = timeout_s.min(TIMEOUT_WHILE_OUTPUT);
497 let mut last_pending_output = String::new();
498 let mut complete = false;
499
500 sleep(Duration::from_secs_f64(wait.min(DEFAULT_TIMEOUT))).await;
502
503 let mut output = {
505 let mut bash_guard = bash_state.interactive_bash.lock().map_err(|e| {
506 WinxError::BashStateLockError(format!("Failed to lock bash state: {e}"))
507 })?;
508
509 if let Some(bash) = bash_guard.as_mut() {
510 let (out, done) = bash.read_output(0.5).map_err(|e| {
511 WinxError::CommandExecutionError(format!("Failed to read output: {e}"))
512 })?;
513 complete = done;
514 out
515 } else {
516 String::new()
517 }
518 };
519
520 if !complete && is_status_check {
523 let mut remaining = TIMEOUT_WHILE_OUTPUT - wait;
524 let mut patience = OUTPUT_WAIT_PATIENCE;
525
526 let incremental = wcgw_incremental_text(&output, &last_pending_output);
527 if incremental.is_empty() {
528 patience -= 1;
529 }
530
531 let mut last_incremental = incremental;
532
533 while remaining > 0.0 && patience > 0 {
534 sleep(Duration::from_secs_f64(wait.min(remaining))).await;
535
536 let (new_output, done) = {
537 let mut bash_guard = bash_state.interactive_bash.lock().map_err(|e| {
538 WinxError::BashStateLockError(format!("Failed to lock bash state: {e}"))
539 })?;
540
541 if let Some(bash) = bash_guard.as_mut() {
542 bash.read_output(0.5).map_err(|e| {
543 WinxError::CommandExecutionError(format!("Failed to read output: {e}"))
544 })?
545 } else {
546 (String::new(), true)
547 }
548 };
549
550 if done {
551 complete = true;
552 output = new_output;
553 break;
554 }
555
556 let new_incremental = wcgw_incremental_text(&new_output, &last_pending_output);
558 if new_incremental == last_incremental {
559 patience -= 1;
560 } else {
561 patience = OUTPUT_WAIT_PATIENCE; }
563 last_incremental = new_incremental;
564
565 output = new_output;
566 remaining -= wait;
567 }
568
569 if !complete {
570 last_pending_output = output.clone();
572 }
573 }
574
575 let rendered = wcgw_incremental_text(&output, &last_pending_output);
577
578 let rendered = if rendered.len() > MAX_OUTPUT_LENGTH {
580 format!("(...truncated)\n{}", &rendered[rendered.len() - MAX_OUTPUT_LENGTH..])
581 } else {
582 rendered
583 };
584
585 let running_for = if complete {
587 None
588 } else {
589 Some(format!("{} seconds", (start.elapsed().as_secs() + timeout_s as u64)))
590 };
591
592 let status = get_status(bash_state, is_bg, bg_id, !complete, running_for.as_deref());
594 Ok(format!("{rendered}{status}"))
595}
596
597async fn execute_status_check(
599 bash_state: &mut BashState,
600 _bg_shell: Option<Arc<StdMutex<Option<InteractiveBash>>>>,
601 is_bg: bool,
602 bg_id: Option<&str>,
603 timeout_s: f64,
604) -> Result<String> {
605 debug!("Processing StatusCheck action");
606
607 let is_running = {
609 let guard = bash_state.interactive_bash.lock().map_err(|e| {
610 WinxError::BashStateLockError(format!("Failed to lock bash state: {e}"))
611 })?;
612 if let Some(ref bash) = *guard {
613 matches!(bash.command_state, CommandState::Running { .. })
614 } else {
615 false
616 }
617 };
618
619 if !is_running && !is_bg {
621 let manager = BG_SHELL_MANAGER.lock().map_err(|e| {
622 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
623 })?;
624 let error =
625 format!("No running command to check status of.\n{}", manager.get_running_info());
626 return Err(WinxError::CommandExecutionError(error));
627 }
628
629 wait_for_output(bash_state, timeout_s, is_bg, bg_id, true).await
631}
632
633async fn execute_send_text(
635 bash_state: &mut BashState,
636 text: &str,
637 bg_shell: Option<Arc<StdMutex<Option<InteractiveBash>>>>,
638 is_bg: bool,
639 bg_id: Option<&str>,
640 timeout_s: f64,
641) -> Result<String> {
642 debug!("Processing SendText action: {}", text);
643
644 if text.is_empty() {
646 return Err(WinxError::CommandExecutionError(
647 "Failure: send_text cannot be empty".to_string(),
648 ));
649 }
650
651 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.interactive_bash.clone());
653
654 {
656 let mut guard = shell_arc
657 .lock()
658 .map_err(|e| WinxError::BashStateLockError(format!("Failed to lock shell: {e}")))?;
659
660 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
661
662 for chunk in text.as_bytes().chunks(TEXT_CHUNK_SIZE) {
664 if let Some(mut stdin) = bash.process.stdin.take() {
665 stdin.write_all(chunk).map_err(|e| {
666 WinxError::CommandExecutionError(format!("Failed to write text chunk: {e}"))
667 })?;
668 bash.process.stdin = Some(stdin);
669 }
670 }
671
672 if let Some(mut stdin) = bash.process.stdin.take() {
674 stdin.write_all(b"\n").map_err(|e| {
675 WinxError::CommandExecutionError(format!("Failed to write newline: {e}"))
676 })?;
677 stdin
678 .flush()
679 .map_err(|e| WinxError::CommandExecutionError(format!("Failed to flush: {e}")))?;
680 bash.process.stdin = Some(stdin);
681 }
682 }
683
684 wait_for_output(bash_state, timeout_s, is_bg, bg_id, false).await
686}
687
688async fn execute_send_specials(
690 bash_state: &mut BashState,
691 keys: &[SpecialKey],
692 bg_shell: Option<Arc<StdMutex<Option<InteractiveBash>>>>,
693 is_bg: bool,
694 bg_id: Option<&str>,
695 timeout_s: f64,
696) -> Result<String> {
697 debug!("Processing SendSpecials action: {:?}", keys);
698
699 if keys.is_empty() {
701 return Err(WinxError::CommandExecutionError(
702 "Failure: send_specials cannot be empty".to_string(),
703 ));
704 }
705
706 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.interactive_bash.clone());
707 let mut is_interrupt = false;
708
709 {
710 let mut guard = shell_arc
711 .lock()
712 .map_err(|e| WinxError::BashStateLockError(format!("Failed to lock shell: {e}")))?;
713
714 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
715
716 for key in keys {
718 match key {
719 SpecialKey::KeyUp => {
720 send_bytes_to_bash(bash, b"\x1b[A")?;
722 }
723 SpecialKey::KeyDown => {
724 send_bytes_to_bash(bash, b"\x1b[B")?;
726 }
727 SpecialKey::KeyLeft => {
728 send_bytes_to_bash(bash, b"\x1b[D")?;
730 }
731 SpecialKey::KeyRight => {
732 send_bytes_to_bash(bash, b"\x1b[C")?;
734 }
735 SpecialKey::Enter => {
736 send_bytes_to_bash(bash, b"\x0d")?;
738 }
739 SpecialKey::CtrlC => {
740 bash.send_interrupt().map_err(|e| {
742 WinxError::CommandExecutionError(format!("Failed to send interrupt: {e}"))
743 })?;
744 is_interrupt = true;
745 }
746 SpecialKey::CtrlD => {
747 bash.send_interrupt().map_err(|e| {
749 WinxError::CommandExecutionError(format!("Failed to send Ctrl+D: {e}"))
750 })?;
751 is_interrupt = true;
752 }
753 SpecialKey::CtrlZ => {
754 send_bytes_to_bash(bash, b"\x1a")?;
756 }
757 }
758 }
759 }
760
761 let mut output = wait_for_output(bash_state, timeout_s, is_bg, bg_id, false).await?;
763
764 if is_interrupt && output.contains("status = still running") {
766 output.push_str("\n---\n----\nFailure interrupting.\nYou may want to try Ctrl-c again or program specific exit interactive commands.\n");
767 }
768
769 Ok(output)
770}
771
772fn send_bytes_to_bash(bash: &mut InteractiveBash, bytes: &[u8]) -> Result<()> {
774 if let Some(mut stdin) = bash.process.stdin.take() {
775 stdin
776 .write_all(bytes)
777 .map_err(|e| WinxError::CommandExecutionError(format!("Failed to write bytes: {e}")))?;
778 stdin
779 .flush()
780 .map_err(|e| WinxError::CommandExecutionError(format!("Failed to flush: {e}")))?;
781 bash.process.stdin = Some(stdin);
782 Ok(())
783 } else {
784 Err(WinxError::CommandExecutionError("Failed to get stdin".to_string()))
785 }
786}
787
788async fn execute_send_ascii(
790 bash_state: &mut BashState,
791 ascii_codes: &[u8],
792 bg_shell: Option<Arc<StdMutex<Option<InteractiveBash>>>>,
793 is_bg: bool,
794 bg_id: Option<&str>,
795 timeout_s: f64,
796) -> Result<String> {
797 debug!("Processing SendAscii action: {:?}", ascii_codes);
798
799 if ascii_codes.is_empty() {
801 return Err(WinxError::CommandExecutionError(
802 "Failure: send_ascii cannot be empty".to_string(),
803 ));
804 }
805
806 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.interactive_bash.clone());
807 let mut is_interrupt = false;
808
809 {
810 let mut guard = shell_arc
811 .lock()
812 .map_err(|e| WinxError::BashStateLockError(format!("Failed to lock shell: {e}")))?;
813
814 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
815
816 for &code in ascii_codes {
818 if let Some(mut stdin) = bash.process.stdin.take() {
820 stdin.write_all(&[code]).map_err(|e| {
821 WinxError::CommandExecutionError(format!("Failed to write ASCII code: {e}"))
822 })?;
823 stdin.flush().map_err(|e| {
824 WinxError::CommandExecutionError(format!("Failed to flush: {e}"))
825 })?;
826 bash.process.stdin = Some(stdin);
827 }
828
829 if code == 3 {
831 is_interrupt = true;
832 }
833 }
834 }
835
836 let mut output = wait_for_output(bash_state, timeout_s, is_bg, bg_id, false).await?;
838
839 if is_interrupt && output.contains("status = still running") {
841 output.push_str("\n---\n----\nFailure interrupting.\nYou may want to try Ctrl-c again or program specific exit interactive commands.\n");
842 }
843
844 Ok(output)
845}
846
847async fn execute_in_background(
849 bash_state: &mut BashState,
850 command: &str,
851 timeout_s: f64,
852) -> Result<String> {
853 debug!("Executing command in background: {}", command);
854
855 let restricted_mode =
857 matches!(bash_state.bash_command_mode.bash_mode, crate::types::BashMode::RestrictedMode);
858
859 let bg_id = {
860 let mut manager = BG_SHELL_MANAGER.lock().map_err(|e| {
861 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
862 })?;
863 manager.start_new_shell(&bash_state.cwd, restricted_mode)?
864 };
865
866 let shell_arc = {
868 let manager = BG_SHELL_MANAGER.lock().map_err(|e| {
869 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
870 })?;
871 manager.get_shell(&bg_id).ok_or_else(|| {
872 WinxError::CommandExecutionError("Failed to get background shell".to_string())
873 })?
874 };
875
876 {
878 let mut guard = shell_arc
879 .lock()
880 .map_err(|e| WinxError::BashStateLockError(format!("Failed to lock bg shell: {e}")))?;
881
882 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
883
884 for chunk in command.as_bytes().chunks(COMMAND_CHUNK_SIZE) {
885 if let Some(mut stdin) = bash.process.stdin.take() {
886 stdin.write_all(chunk).map_err(|e| {
887 WinxError::CommandExecutionError(format!("Failed to write chunk: {e}"))
888 })?;
889 bash.process.stdin = Some(stdin);
890 }
891 }
892
893 if let Some(mut stdin) = bash.process.stdin.take() {
894 stdin.write_all(b"\n").map_err(|e| {
895 WinxError::CommandExecutionError(format!("Failed to write newline: {e}"))
896 })?;
897 stdin
898 .flush()
899 .map_err(|e| WinxError::CommandExecutionError(format!("Failed to flush: {e}")))?;
900 bash.process.stdin = Some(stdin);
901 }
902
903 bash.last_command = command.to_string();
904 bash.command_state = CommandState::Running {
905 start_time: std::time::SystemTime::now(),
906 command: command.to_string(),
907 };
908 }
909
910 sleep(Duration::from_secs_f64(timeout_s.min(DEFAULT_TIMEOUT))).await;
912
913 let (output, complete) = {
914 let mut guard = shell_arc
915 .lock()
916 .map_err(|e| WinxError::BashStateLockError(format!("Failed to lock bg shell: {e}")))?;
917
918 if let Some(bash) = guard.as_mut() {
919 bash.read_output(0.5).map_err(|e| {
920 WinxError::CommandExecutionError(format!("Failed to read output: {e}"))
921 })?
922 } else {
923 (String::new(), true)
924 }
925 };
926
927 let rendered = wcgw_incremental_text(&output, "");
929 let rendered = if rendered.len() > MAX_OUTPUT_LENGTH {
930 format!("(...truncated)\n{}", &rendered[rendered.len() - MAX_OUTPUT_LENGTH..])
931 } else {
932 rendered
933 };
934
935 let status = get_status(bash_state, true, Some(&bg_id), !complete, None);
937
938 if complete {
940 let mut manager = BG_SHELL_MANAGER.lock().map_err(|e| {
941 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
942 })?;
943 manager.remove_shell(&bg_id);
944 }
945
946 Ok(format!("{rendered}{status}"))
947}
948
949#[allow(dead_code)]
953#[tracing::instrument(level = "debug", skip(command, cwd))]
954async fn execute_simple_command(command: &str, cwd: &Path) -> Result<String> {
955 debug!("Executing command: {}", command);
956
957 let start_time = Instant::now();
958 let mut cmd = Command::new("sh");
959 cmd.arg("-c")
960 .arg(command)
961 .current_dir(cwd)
962 .stdin(Stdio::null())
963 .stdout(Stdio::piped())
964 .stderr(Stdio::piped());
965
966 let output = cmd.output().context("Failed to execute command")?;
967 let elapsed = start_time.elapsed();
968
969 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
970 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
971
972 let raw_result = format!("{stdout}{stderr}");
973 let mut result = raw_result.clone();
974 if !raw_result.is_empty() {
975 let rendered_lines = render_terminal_output(&raw_result);
976 if rendered_lines.is_empty() {
977 result = strip_ansi_codes(&raw_result);
979 } else {
980 result = rendered_lines.join("\n");
981 }
982 }
983
984 if result.len() > MAX_OUTPUT_LENGTH {
985 result = format!("(...truncated)\n{}", &result[result.len() - MAX_OUTPUT_LENGTH..]);
986 }
987
988 let exit_status = if output.status.success() {
989 "Command completed successfully".to_string()
990 } else {
991 format!("Command failed with status: {}", output.status)
992 };
993
994 let current_dir = std::env::current_dir()
995 .map_or_else(|_| "Unknown".to_string(), |p| p.to_string_lossy().into_owned());
996
997 debug!("Command executed in {:.2?}", elapsed);
998 Ok(format!("{result}\n\n---\n\nstatus = {exit_status}\ncwd = {current_dir}\n"))
999}
1000
1001#[allow(dead_code)]
1003#[tracing::instrument(level = "debug", skip(command, cwd, screen_name))]
1004async fn execute_in_screen(command: &str, cwd: &Path, screen_name: &str) -> Result<String> {
1005 debug!("Executing command in screen session '{}': {}", screen_name, command);
1006
1007 let screen_check = Command::new("which")
1008 .arg("screen")
1009 .output()
1010 .context("Failed to check for screen command")?;
1011
1012 if !screen_check.status.success() {
1013 warn!("Screen command not found, falling back to direct execution");
1014 return execute_simple_command(command, cwd).await;
1015 }
1016
1017 let _cleanup = Command::new("screen").args(["-X", "-S", screen_name, "quit"]).output();
1018
1019 let screen_cmd = format!(
1020 "screen -dmS {} bash -c '{} ; ec=$? ; echo \"Command completed with exit code: $ec\" ; sleep 1 ; exit $ec'",
1021 screen_name,
1022 command.replace('\'', "'\\''")
1023 );
1024
1025 let screen_start = Command::new("sh")
1026 .arg("-c")
1027 .arg(&screen_cmd)
1028 .current_dir(cwd)
1029 .output()
1030 .context("Failed to start screen session")?;
1031
1032 if !screen_start.status.success() {
1033 let stderr = String::from_utf8_lossy(&screen_start.stderr).to_string();
1034 error!("Failed to start screen session: {}", stderr);
1035 return Err(WinxError::CommandExecutionError(format!(
1036 "Failed to start screen session: {stderr}"
1037 )));
1038 }
1039
1040 sleep(Duration::from_millis(300)).await;
1041
1042 let screen_check =
1043 Command::new("screen").args(["-ls"]).output().context("Failed to list screen sessions")?;
1044
1045 let screen_list = String::from_utf8_lossy(&screen_check.stdout).to_string();
1046
1047 let current_dir = std::env::current_dir()
1048 .map_or_else(|_| "Unknown".to_string(), |p| p.to_string_lossy().into_owned());
1049
1050 Ok(format!(
1051 "Started command in background screen session '{screen_name}'.\n\
1052 Use status_check to get output.\n\n\
1053 Screen sessions:\n{screen_list}\n\
1054 ---\n\n\
1055 status = running in background\n\
1056 cwd = {current_dir}\n"
1057 ))
1058}
1059
1060#[allow(dead_code)]
1062fn special_key_to_screen_input(key: SpecialKey) -> String {
1063 match key {
1064 SpecialKey::Enter => String::from("\r"),
1065 SpecialKey::KeyUp => String::from("\x1b[A"),
1066 SpecialKey::KeyDown => String::from("\x1b[B"),
1067 SpecialKey::KeyLeft => String::from("\x1b[D"),
1068 SpecialKey::KeyRight => String::from("\x1b[C"),
1069 SpecialKey::CtrlC => String::from("\x03"),
1070 SpecialKey::CtrlD => String::from("\x04"),
1071 SpecialKey::CtrlZ => String::from("\x1a"),
1072 }
1073}