1use anyhow::Context as AnyhowContext;
8use rand::RngExt;
9use std::collections::HashMap;
10use std::fmt::Write as FmtWrite;
11use std::path::Path;
12use std::process::{Command, Stdio};
13use std::sync::{Arc, Mutex as StdMutex};
14use std::time::{Duration, Instant};
15use tokio::sync::Mutex;
16use tokio::time::sleep;
17use tracing::{debug, error, info, warn};
18
19use crate::errors::{Result, WinxError};
20use crate::state::bash_state::BashState;
21use crate::state::pty::PtyShell;
22use crate::state::terminal::{render_terminal_output, strip_ansi_codes};
23use crate::types::{normalize_thread_id, BashCommand, BashCommandAction, SpecialKey};
24
25type SharedPtyShell = Arc<Mutex<Option<PtyShell>>>;
26
27const DEFAULT_TIMEOUT: f64 = 5.0;
31
32const TIMEOUT_WHILE_OUTPUT: f64 = 20.0;
34
35const OUTPUT_WAIT_PATIENCE: i32 = 3;
37
38const COMMAND_CHUNK_SIZE: usize = 64;
40
41const TEXT_CHUNK_SIZE: usize = 128;
43
44const MAX_OUTPUT_LENGTH: usize = 100_000;
46
47const 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.
491. Get its output using status check.
502. Use `send_ascii` or `send_specials` to give inputs to the running program OR
513. kill the previous program by sending ctrl+c first using `send_ascii` or `send_specials`
524. Interrupt and run the process in background
53";
54
55#[derive(Debug, Default)]
59pub struct BackgroundShellManager {
60 shells: HashMap<String, SharedPtyShell>,
61}
62
63impl BackgroundShellManager {
64 pub fn new() -> Self {
66 Self { shells: HashMap::new() }
67 }
68
69 pub fn start_new_shell(&mut self, working_dir: &Path, restricted_mode: bool) -> Result<String> {
71 let cid = format!("{:010x}", rand::rng().random::<u32>());
72
73 let shell = PtyShell::new(working_dir, restricted_mode).map_err(|e| {
74 WinxError::CommandExecutionError(format!("Failed to start background shell: {e}"))
75 })?;
76
77 self.shells.insert(cid.clone(), Arc::new(Mutex::new(Some(shell))));
78
79 info!("Started background shell with id: {}", cid);
80 Ok(cid)
81 }
82
83 pub fn get_shell(&self, bg_command_id: &str) -> Option<SharedPtyShell> {
85 self.shells.get(bg_command_id).cloned()
86 }
87
88 pub fn remove_shell(&mut self, bg_command_id: &str) -> bool {
90 if let Some(shell_arc) = self.shells.remove(bg_command_id) {
91 if let Ok(mut guard) = shell_arc.try_lock() {
92 *guard = None;
93 }
94 info!("Removed background shell: {}", bg_command_id);
95 true
96 } else {
97 false
98 }
99 }
100
101 pub fn get_running_info(&self) -> String {
103 if self.shells.is_empty() {
104 return "No command running in background.\n".to_string();
105 }
106
107 let mut running = Vec::new();
108 for (id, shell_arc) in &self.shells {
109 if let Ok(guard) = shell_arc.try_lock() {
110 if let Some(bash) = guard.as_ref() {
111 running.push(format!("Command: {}, bg_command_id: {}", bash.last_command, id));
112 }
113 } else {
114 running.push(format!("Command: <busy>, bg_command_id: {id}"));
115 }
116 }
117
118 if running.is_empty() {
119 "No command running in background.\n".to_string()
120 } else {
121 format!("Following background commands are attached:\n{}\n", running.join("\n"))
122 }
123 }
124}
125
126lazy_static::lazy_static! {
128 static ref BG_SHELL_MANAGER: StdMutex<BackgroundShellManager> = StdMutex::new(BackgroundShellManager::new());
129}
130
131fn get_status(
135 bash_state: &BashState,
136 is_bg: bool,
137 bg_id: Option<&str>,
138 is_running: bool,
139 running_for: Option<&str>,
140) -> String {
141 let mut status = "\n\n---\n\n".to_string();
142
143 if is_bg {
144 if let Some(id) = bg_id {
145 let _ = writeln!(status, "bg_command_id = {id}");
146 }
147 }
148
149 if is_running {
150 status.push_str("status = still running\n");
151 if let Some(duration) = running_for {
152 let _ = writeln!(status, "running for = {duration}");
153 }
154 } else {
155 status.push_str("status = process exited\n");
156 }
157
158 let _ = writeln!(status, "cwd = {}", bash_state.cwd.display());
159
160 if !is_bg {
161 if let Ok(manager) = BG_SHELL_MANAGER.lock() {
163 status.push_str("This is the main shell. ");
164 status.push_str(&manager.get_running_info());
165 }
166 }
167
168 status.trim_end().to_string()
169}
170
171fn wcgw_incremental_text(text: &str, last_pending_output: &str) -> String {
173 let text =
174 if text.len() > MAX_OUTPUT_LENGTH { &text[text.len() - MAX_OUTPUT_LENGTH..] } else { text };
175
176 if last_pending_output.is_empty() {
177 let rendered = render_terminal_output(text);
178 return rstrip_lines(&rendered).trim_start().to_string();
179 }
180
181 let last_rendered = render_terminal_output(last_pending_output);
182 if last_rendered.is_empty() {
183 return rstrip_lines(&render_terminal_output(text));
184 }
185
186 let text_after_last = if text.len() > last_pending_output.len() {
188 &text[last_pending_output.len()..]
189 } else {
190 text
191 };
192
193 let combined = format!("{}\n{}", last_rendered.join("\n"), text_after_last);
194 let new_rendered = render_terminal_output(&combined);
195
196 let incremental = get_incremental_output(&last_rendered, &new_rendered);
198 rstrip_lines(&incremental)
199}
200
201fn rstrip_lines(lines: &[String]) -> String {
203 lines.iter().map(|line| line.trim_end()).collect::<Vec<_>>().join("\n")
204}
205
206fn get_incremental_output(old_output: &[String], new_output: &[String]) -> Vec<String> {
208 if old_output.is_empty() {
209 return new_output.to_vec();
210 }
211
212 let nold = old_output.len();
213 let nnew = new_output.len();
214
215 for i in (0..nnew).rev() {
217 if new_output[i] != old_output[nold - 1] {
218 continue;
219 }
220
221 let mut matched = true;
222 for j in (0..i).rev() {
223 let old_idx = (nold as i64 - 1 + j as i64 - i as i64) as isize;
224 if old_idx < 0 {
225 break;
226 }
227 if new_output[j] != old_output[old_idx as usize] {
228 matched = false;
229 break;
230 }
231 }
232
233 if matched {
234 return new_output[i + 1..].to_vec();
235 }
236 }
237
238 new_output.to_vec()
239}
240
241fn send_utf8_in_byte_chunks(shell: &mut PtyShell, text: &str, chunk_size: usize) -> Result<()> {
242 let mut start = 0;
243
244 while start < text.len() {
245 let mut end = (start + chunk_size).min(text.len());
246 while !text.is_char_boundary(end) {
247 end -= 1;
248 }
249 if end == start {
250 end = text[start..].char_indices().nth(1).map_or(text.len(), |(idx, _)| start + idx);
251 }
252
253 shell.send_text(&text[start..end]).map_err(|e| {
254 WinxError::CommandExecutionError(format!("Failed to write PTY input: {e}"))
255 })?;
256 start = end;
257 }
258
259 Ok(())
260}
261
262#[allow(dead_code)]
264fn is_status_check_action(action: &BashCommandAction) -> bool {
265 match action {
266 BashCommandAction::StatusCheck { .. } => true,
267 BashCommandAction::SendSpecials { send_specials, .. } => {
268 send_specials.len() == 1 && send_specials[0] == SpecialKey::Enter
269 }
270 BashCommandAction::SendAscii { send_ascii, .. } => {
271 send_ascii.len() == 1 && send_ascii[0] == 10 }
273 _ => false,
274 }
275}
276
277#[tracing::instrument(level = "info", skip(bash_state_arc, bash_command))]
284pub async fn handle_tool_call(
285 bash_state_arc: &Arc<Mutex<Option<BashState>>>,
286 bash_command: BashCommand,
287) -> Result<String> {
288 info!("BashCommand tool called with: {:?}", bash_command);
289
290 let thread_id = normalize_thread_id(&bash_command.thread_id);
291
292 if thread_id.is_empty() {
294 error!("Empty thread_id provided in BashCommand");
295 return Err(WinxError::ThreadIdMismatch(
296 "Error: No saved bash state found for thread ID \"\". Please initialize first with this ID.".to_string()
297 ));
298 }
299
300 let mut bash_state: BashState;
302 {
303 let bash_state_guard = bash_state_arc.lock().await;
304
305 let Some(state) = &*bash_state_guard else {
306 error!("BashState not initialized");
307 return Err(WinxError::BashStateNotInitialized);
308 };
309
310 bash_state = state.clone();
311 }
312
313 if thread_id != bash_state.current_thread_id {
315 if !bash_state.load_state_from_disk(&thread_id).unwrap_or(false) {
317 return Err(WinxError::ThreadIdMismatch(format!(
318 "Error: No saved bash state found for thread_id `{thread_id}`. Please initialize first with this ID."
319 )));
320 }
321 }
322
323 let timeout_s = bash_command
326 .wait_for_seconds
327 .map_or(DEFAULT_TIMEOUT, |t| f64::from(t).max(0.0))
328 .min(TIMEOUT_WHILE_OUTPUT);
329
330 let result = execute_bash_action(&mut bash_state, &bash_command.action_json, timeout_s).await;
332
333 match result {
335 Ok(mut output) => {
336 if let BashCommandAction::Command { ref command, .. } = bash_command.action_json {
337 let cmd_trimmed = command.trim();
338 if output.starts_with(cmd_trimmed) {
339 output = output[cmd_trimmed.len()..].to_string();
340 }
341 }
342 Ok(output)
343 }
344 Err(e) => Err(e),
345 }
346}
347
348async fn execute_bash_action(
350 bash_state: &mut BashState,
351 action: &BashCommandAction,
352 timeout_s: f64,
353) -> Result<String> {
354 let mut is_bg = false;
355 let mut bg_id: Option<String> = None;
356
357 let bg_shell: Option<SharedPtyShell> = match action {
359 BashCommandAction::Command { .. } => None, BashCommandAction::StatusCheck { bg_command_id, .. }
361 | BashCommandAction::SendText { bg_command_id, .. }
362 | BashCommandAction::SendSpecials { bg_command_id, .. }
363 | BashCommandAction::SendAscii { bg_command_id, .. } => {
364 if let Some(id) = bg_command_id {
365 let manager = BG_SHELL_MANAGER.lock().map_err(|e| {
366 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
367 })?;
368
369 if let Some(shell) = manager.get_shell(id) {
370 is_bg = true;
371 bg_id = Some(id.clone());
372 Some(shell)
373 } else {
374 let error = format!(
376 "No shell found running with command id {}.\n{}",
377 id,
378 manager.get_running_info()
379 );
380 return Err(WinxError::CommandExecutionError(error));
381 }
382 } else {
383 None
384 }
385 }
386 };
387
388 match action {
390 BashCommandAction::Command { command, is_background } => {
391 execute_command(bash_state, command, *is_background, timeout_s).await
392 }
393 BashCommandAction::StatusCheck { .. } => {
394 execute_status_check(bash_state, bg_shell, is_bg, bg_id.as_deref(), timeout_s).await
395 }
396 BashCommandAction::SendText { send_text, .. } => {
397 execute_send_text(bash_state, send_text, bg_shell, is_bg, bg_id.as_deref(), timeout_s)
398 .await
399 }
400 BashCommandAction::SendSpecials { send_specials, .. } => {
401 execute_send_specials(
402 bash_state,
403 send_specials,
404 bg_shell,
405 is_bg,
406 bg_id.as_deref(),
407 timeout_s,
408 )
409 .await
410 }
411 BashCommandAction::SendAscii { send_ascii, .. } => {
412 execute_send_ascii(bash_state, send_ascii, bg_shell, is_bg, bg_id.as_deref(), timeout_s)
413 .await
414 }
415 }
416}
417
418async fn execute_command(
420 bash_state: &mut BashState,
421 command: &str,
422 is_background: bool,
423 timeout_s: f64,
424) -> Result<String> {
425 debug!("Processing Command action: {}", command);
426
427 if !bash_state.is_command_allowed(command) {
429 error!("Command '{}' not allowed in current mode", command);
430 return Err(WinxError::CommandNotAllowed(
431 "Error: BashCommand not allowed in current mode".to_string(),
432 ));
433 }
434
435 let command = command.trim();
437 crate::utils::bash_parser::assert_single_statement(command)?;
438
439 if is_background {
441 return execute_in_background(bash_state, command, timeout_s).await;
442 }
443
444 {
446 let bash_guard = bash_state.pty_shell.lock().await;
447
448 if let Some(ref bash) = *bash_guard {
449 if bash.command_running {
450 return Err(WinxError::CommandExecutionError(WAITING_INPUT_MESSAGE.to_string()));
451 }
452 }
453 }
454
455 if bash_state.pty_shell.lock().await.is_none() {
457 bash_state
458 .init_pty_shell()
459 .await
460 .map_err(|e| WinxError::CommandExecutionError(format!("Failed to init bash: {e}")))?;
461 }
462
463 {
468 let mut bash_guard = bash_state.pty_shell.lock().await;
469
470 let bash = bash_guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
471
472 bash.output_buffer.clear();
473 bash.output_truncated = false;
474 send_utf8_in_byte_chunks(bash, command, COMMAND_CHUNK_SIZE)?;
476
477 bash.send_special_key("Enter").map_err(|e| {
479 WinxError::CommandExecutionError(format!("Failed to send newline: {e}"))
480 })?;
481
482 bash.last_command = command.to_string();
483 bash.command_running = true;
484 }
485
486 let shell_arc = bash_state.pty_shell.clone();
488 wait_for_output(bash_state, &shell_arc, timeout_s, false, None, false).await
489}
490
491async fn wait_for_output(
495 bash_state: &BashState,
496 shell_arc: &SharedPtyShell,
497 timeout_s: f64,
498 is_bg: bool,
499 bg_id: Option<&str>,
500 is_status_check: bool,
501) -> Result<String> {
502 let start = Instant::now();
503 let wait = timeout_s.min(TIMEOUT_WHILE_OUTPUT);
504 let mut last_pending_output = String::new();
505 let mut complete = false;
506
507 sleep(Duration::from_secs_f64(wait.min(DEFAULT_TIMEOUT))).await;
509
510 let mut output = {
512 let mut bash_guard = shell_arc.lock().await;
513
514 if let Some(bash) = bash_guard.as_mut() {
515 let (out, done) = bash.read_output(0.5).map_err(|e| {
516 WinxError::CommandExecutionError(format!("Failed to read output: {e}"))
517 })?;
518 complete = done;
519 out
520 } else {
521 String::new()
522 }
523 };
524
525 if !complete && is_status_check {
528 let mut remaining = TIMEOUT_WHILE_OUTPUT - wait;
529 let mut patience = OUTPUT_WAIT_PATIENCE;
530
531 let incremental = wcgw_incremental_text(&output, &last_pending_output);
532 if incremental.is_empty() {
533 patience -= 1;
534 }
535
536 let mut last_incremental = incremental;
537
538 while remaining > 0.0 && patience > 0 {
539 sleep(Duration::from_secs_f64(wait.min(remaining))).await;
540
541 let (new_output, done) = {
542 let mut bash_guard = shell_arc.lock().await;
543
544 if let Some(bash) = bash_guard.as_mut() {
545 bash.read_output(0.5).map_err(|e| {
546 WinxError::CommandExecutionError(format!("Failed to read output: {e}"))
547 })?
548 } else {
549 (String::new(), true)
550 }
551 };
552
553 if done {
554 complete = true;
555 output = new_output;
556 break;
557 }
558
559 let new_incremental = wcgw_incremental_text(&new_output, &last_pending_output);
561 if new_incremental == last_incremental {
562 patience -= 1;
563 } else {
564 patience = OUTPUT_WAIT_PATIENCE; }
566 last_incremental = new_incremental;
567
568 output = new_output;
569 remaining -= wait;
570 }
571
572 if !complete {
573 last_pending_output = output.clone();
575 }
576 }
577
578 let rendered = wcgw_incremental_text(&output, &last_pending_output);
580
581 let rendered = if rendered.len() > MAX_OUTPUT_LENGTH {
583 format!("(...truncated)\n{}", &rendered[rendered.len() - MAX_OUTPUT_LENGTH..])
584 } else {
585 rendered
586 };
587
588 let running_for = if complete {
590 None
591 } else {
592 Some(format!("{} seconds", (start.elapsed().as_secs() + timeout_s as u64)))
593 };
594
595 let status = get_status(bash_state, is_bg, bg_id, !complete, running_for.as_deref());
597 Ok(format!("{rendered}{status}"))
598}
599
600async fn execute_status_check(
602 bash_state: &mut BashState,
603 bg_shell: Option<SharedPtyShell>,
604 is_bg: bool,
605 bg_id: Option<&str>,
606 timeout_s: f64,
607) -> Result<String> {
608 debug!("Processing StatusCheck action");
609
610 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.pty_shell.clone());
613
614 let is_running = {
616 let guard = shell_arc.lock().await;
617 if let Some(ref bash) = *guard {
618 bash.command_running
619 } else {
620 false
621 }
622 };
623
624 if !is_running && !is_bg {
626 let manager = BG_SHELL_MANAGER.lock().map_err(|e| {
627 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
628 })?;
629 let error =
630 format!("No running command to check status of.\n{}", manager.get_running_info());
631 return Err(WinxError::CommandExecutionError(error));
632 }
633
634 wait_for_output(bash_state, &shell_arc, timeout_s, is_bg, bg_id, true).await
636}
637
638async fn execute_send_text(
640 bash_state: &mut BashState,
641 text: &str,
642 bg_shell: Option<SharedPtyShell>,
643 is_bg: bool,
644 bg_id: Option<&str>,
645 timeout_s: f64,
646) -> Result<String> {
647 debug!("Processing SendText action: {}", text);
648
649 if text.is_empty() {
651 return Err(WinxError::CommandExecutionError(
652 "Failure: send_text cannot be empty".to_string(),
653 ));
654 }
655
656 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.pty_shell.clone());
658
659 {
661 let mut guard = shell_arc.lock().await;
662
663 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
664
665 send_utf8_in_byte_chunks(bash, text, TEXT_CHUNK_SIZE)?;
667
668 bash.send_special_key("Enter").map_err(|e| {
670 WinxError::CommandExecutionError(format!("Failed to send newline: {e}"))
671 })?;
672 }
673
674 wait_for_output(bash_state, &shell_arc, timeout_s, is_bg, bg_id, false).await
676}
677
678async fn execute_send_specials(
680 bash_state: &mut BashState,
681 keys: &[SpecialKey],
682 bg_shell: Option<SharedPtyShell>,
683 is_bg: bool,
684 bg_id: Option<&str>,
685 timeout_s: f64,
686) -> Result<String> {
687 debug!("Processing SendSpecials action: {:?}", keys);
688
689 if keys.is_empty() {
691 return Err(WinxError::CommandExecutionError(
692 "Failure: send_specials cannot be empty".to_string(),
693 ));
694 }
695
696 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.pty_shell.clone());
697 let mut is_interrupt = false;
698
699 {
700 let mut guard = shell_arc.lock().await;
701
702 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
703
704 for key in keys {
706 match key {
707 SpecialKey::KeyUp => {
708 bash.send_special_key("KeyUp").map_err(|e| {
710 WinxError::CommandExecutionError(format!("Failed to send KeyUp: {e}"))
711 })?;
712 }
713 SpecialKey::KeyDown => {
714 bash.send_special_key("KeyDown").map_err(|e| {
716 WinxError::CommandExecutionError(format!("Failed to send KeyDown: {e}"))
717 })?;
718 }
719 SpecialKey::KeyLeft => {
720 bash.send_special_key("KeyLeft").map_err(|e| {
722 WinxError::CommandExecutionError(format!("Failed to send KeyLeft: {e}"))
723 })?;
724 }
725 SpecialKey::KeyRight => {
726 bash.send_special_key("KeyRight").map_err(|e| {
728 WinxError::CommandExecutionError(format!("Failed to send KeyRight: {e}"))
729 })?;
730 }
731 SpecialKey::Enter => {
732 bash.send_special_key("Enter").map_err(|e| {
734 WinxError::CommandExecutionError(format!("Failed to send Enter: {e}"))
735 })?;
736 }
737 SpecialKey::CtrlC => {
738 bash.send_interrupt().map_err(|e| {
740 WinxError::CommandExecutionError(format!("Failed to send interrupt: {e}"))
741 })?;
742 is_interrupt = true;
743 }
744 SpecialKey::CtrlD => {
745 bash.send_eof().map_err(|e| {
747 WinxError::CommandExecutionError(format!("Failed to send Ctrl+D: {e}"))
748 })?;
749 is_interrupt = true;
750 }
751 SpecialKey::CtrlZ => {
752 bash.send_suspend().map_err(|e| {
754 WinxError::CommandExecutionError(format!("Failed to send Ctrl+Z: {e}"))
755 })?;
756 }
757 }
758 }
759 }
760
761 let mut output =
763 wait_for_output(bash_state, &shell_arc, timeout_s, is_bg, bg_id, false).await?;
764
765 if is_interrupt && output.contains("status = still running") {
767 output.push_str("\n---\n----\nFailure interrupting.\nYou may want to try Ctrl-c again or program specific exit interactive commands.\n");
768 }
769
770 Ok(output)
771}
772
773async fn execute_send_ascii(
775 bash_state: &mut BashState,
776 ascii_codes: &[u8],
777 bg_shell: Option<SharedPtyShell>,
778 is_bg: bool,
779 bg_id: Option<&str>,
780 timeout_s: f64,
781) -> Result<String> {
782 debug!("Processing SendAscii action: {:?}", ascii_codes);
783
784 if ascii_codes.is_empty() {
786 return Err(WinxError::CommandExecutionError(
787 "Failure: send_ascii cannot be empty".to_string(),
788 ));
789 }
790
791 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.pty_shell.clone());
792 let mut is_interrupt = false;
793
794 {
795 let mut guard = shell_arc.lock().await;
796
797 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
798
799 for &code in ascii_codes {
801 bash.send_bytes(&[code]).map_err(|e| {
803 WinxError::CommandExecutionError(format!("Failed to write ASCII code: {e}"))
804 })?;
805
806 if code == 3 {
808 is_interrupt = true;
809 }
810 }
811 }
812
813 let mut output =
815 wait_for_output(bash_state, &shell_arc, timeout_s, is_bg, bg_id, false).await?;
816
817 if is_interrupt && output.contains("status = still running") {
819 output.push_str("\n---\n----\nFailure interrupting.\nYou may want to try Ctrl-c again or program specific exit interactive commands.\n");
820 }
821
822 Ok(output)
823}
824
825async fn execute_in_background(
827 bash_state: &mut BashState,
828 command: &str,
829 timeout_s: f64,
830) -> Result<String> {
831 debug!("Executing command in background: {}", command);
832
833 let restricted_mode =
835 matches!(bash_state.bash_command_mode.bash_mode, crate::types::BashMode::RestrictedMode);
836
837 let bg_id = {
838 let mut manager = BG_SHELL_MANAGER.lock().map_err(|e| {
839 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
840 })?;
841 manager.start_new_shell(&bash_state.cwd, restricted_mode)?
842 };
843
844 let shell_arc = {
846 let manager = BG_SHELL_MANAGER.lock().map_err(|e| {
847 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
848 })?;
849 manager.get_shell(&bg_id).ok_or_else(|| {
850 WinxError::CommandExecutionError("Failed to get background shell".to_string())
851 })?
852 };
853
854 {
856 let mut guard = shell_arc.lock().await;
857 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
858 bash.send_command(command).map_err(|e| {
859 WinxError::CommandExecutionError(format!("Failed to send bg command: {e}"))
860 })?;
861 }
862 debug!("bg[{}]: send_command returned, replying with bg_command_id", bg_id);
863
864 let _ = timeout_s;
865 let _ = shell_arc;
866 Ok(get_status(bash_state, true, Some(&bg_id), true, None))
867}
868
869#[allow(dead_code)]
873#[tracing::instrument(level = "debug", skip(command, cwd))]
874async fn execute_simple_command(command: &str, cwd: &Path) -> Result<String> {
875 debug!("Executing command: {}", command);
876
877 let start_time = Instant::now();
878 let mut cmd = Command::new("sh");
879 cmd.arg("-c")
880 .arg(command)
881 .current_dir(cwd)
882 .stdin(Stdio::null())
883 .stdout(Stdio::piped())
884 .stderr(Stdio::piped());
885
886 let output = cmd.output().context("Failed to execute command")?;
887 let elapsed = start_time.elapsed();
888
889 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
890 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
891
892 let raw_result = format!("{stdout}{stderr}");
893 let mut result = raw_result.clone();
894 if !raw_result.is_empty() {
895 let rendered_lines = render_terminal_output(&raw_result);
896 if rendered_lines.is_empty() {
897 result = strip_ansi_codes(&raw_result);
899 } else {
900 result = rendered_lines.join("\n");
901 }
902 }
903
904 if result.len() > MAX_OUTPUT_LENGTH {
905 result = format!("(...truncated)\n{}", &result[result.len() - MAX_OUTPUT_LENGTH..]);
906 }
907
908 let exit_status = if output.status.success() {
909 "Command completed successfully".to_string()
910 } else {
911 format!("Command failed with status: {}", output.status)
912 };
913
914 let current_dir = std::env::current_dir()
915 .map_or_else(|_| "Unknown".to_string(), |p| p.to_string_lossy().into_owned());
916
917 debug!("Command executed in {:.2?}", elapsed);
918 Ok(format!("{result}\n\n---\n\nstatus = {exit_status}\ncwd = {current_dir}\n"))
919}
920
921#[allow(dead_code)]
923#[tracing::instrument(level = "debug", skip(command, cwd, screen_name))]
924async fn execute_in_screen(command: &str, cwd: &Path, screen_name: &str) -> Result<String> {
925 debug!("Executing command in screen session '{}': {}", screen_name, command);
926
927 let screen_check = Command::new("which")
928 .arg("screen")
929 .output()
930 .context("Failed to check for screen command")?;
931
932 if !screen_check.status.success() {
933 warn!("Screen command not found, falling back to direct execution");
934 return execute_simple_command(command, cwd).await;
935 }
936
937 let _cleanup = Command::new("screen").args(["-X", "-S", screen_name, "quit"]).output();
938
939 let screen_cmd = format!(
940 "screen -dmS {} bash -c '{} ; ec=$? ; echo \"Command completed with exit code: $ec\" ; sleep 1 ; exit $ec'",
941 screen_name,
942 command.replace('\'', "'\\''")
943 );
944
945 let screen_start = Command::new("sh")
946 .arg("-c")
947 .arg(&screen_cmd)
948 .current_dir(cwd)
949 .output()
950 .context("Failed to start screen session")?;
951
952 if !screen_start.status.success() {
953 let stderr = String::from_utf8_lossy(&screen_start.stderr).to_string();
954 error!("Failed to start screen session: {}", stderr);
955 return Err(WinxError::CommandExecutionError(format!(
956 "Failed to start screen session: {stderr}"
957 )));
958 }
959
960 sleep(Duration::from_millis(300)).await;
961
962 let screen_check =
963 Command::new("screen").args(["-ls"]).output().context("Failed to list screen sessions")?;
964
965 let screen_list = String::from_utf8_lossy(&screen_check.stdout).to_string();
966
967 let current_dir = std::env::current_dir()
968 .map_or_else(|_| "Unknown".to_string(), |p| p.to_string_lossy().into_owned());
969
970 Ok(format!(
971 "Started command in background screen session '{screen_name}'.\n\
972 Use status_check to get output.\n\n\
973 Screen sessions:\n{screen_list}\n\
974 ---\n\n\
975 status = running in background\n\
976 cwd = {current_dir}\n"
977 ))
978}
979
980#[allow(dead_code)]
982fn special_key_to_screen_input(key: SpecialKey) -> String {
983 match key {
984 SpecialKey::Enter => String::from("\r"),
985 SpecialKey::KeyUp => String::from("\x1b[A"),
986 SpecialKey::KeyDown => String::from("\x1b[B"),
987 SpecialKey::KeyLeft => String::from("\x1b[D"),
988 SpecialKey::KeyRight => String::from("\x1b[C"),
989 SpecialKey::CtrlC => String::from("\x03"),
990 SpecialKey::CtrlD => String::from("\x04"),
991 SpecialKey::CtrlZ => String::from("\x1a"),
992 }
993}