Skip to main content

winx_code_agent/tools/
bash_command.rs

1//! Implementation of the `BashCommand` tool with WCGW parity.
2//!
3//! This module provides the implementation for the `BashCommand` tool, which is used
4//! to execute shell commands, check command status, and interact with the shell.
5//! Matches the behavior of wcgw Python implementation 1:1.
6
7use 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
25// ==================== WCGW-Style Constants ====================
26
27/// Default timeout for command execution (seconds) - matches WCGW Python Config.timeout
28const DEFAULT_TIMEOUT: f64 = 5.0;
29
30/// Extended timeout while output is still being produced - matches WCGW Python `Config.timeout_while_output`
31const TIMEOUT_WHILE_OUTPUT: f64 = 20.0;
32
33/// Number of iterations to wait without new output before giving up - matches WCGW Python `Config.output_wait_patience`
34const OUTPUT_WAIT_PATIENCE: i32 = 3;
35
36/// Chunk size for sending commands (characters) - matches WCGW Python (64 chars)
37const COMMAND_CHUNK_SIZE: usize = 64;
38
39/// Chunk size for sending text input (characters) - matches WCGW Python (128 chars)
40const TEXT_CHUNK_SIZE: usize = 128;
41
42/// Maximum output length to prevent excessive responses
43const MAX_OUTPUT_LENGTH: usize = 100_000;
44
45/// Message when a command is already running - matches WCGW Python `WAITING_INPUT_MESSAGE`
46const 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// ==================== Background Shell Manager ====================
54
55/// Manages background shell sessions - matches WCGW Python's `background_shells` dict
56#[derive(Debug, Default)]
57pub struct BackgroundShellManager {
58    shells: HashMap<String, Arc<StdMutex<Option<InteractiveBash>>>>,
59}
60
61impl BackgroundShellManager {
62    /// Create a new background shell manager
63    pub fn new() -> Self {
64        Self { shells: HashMap::new() }
65    }
66
67    /// Start a new background shell and return its command ID
68    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    /// Get a background shell by its command ID
82    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    /// Remove and cleanup a background shell
87    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; // Drop the shell
91            }
92            info!("Removed background shell: {}", bg_command_id);
93            true
94        } else {
95            false
96        }
97    }
98
99    /// Get info about all running background shells - matches WCGW Python `get_bg_running_commandsinfo`
100    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
122// Global background shell manager (thread-safe) - matches WCGW Python's BashState.background_shells
123lazy_static::lazy_static! {
124    static ref BG_SHELL_MANAGER: StdMutex<BackgroundShellManager> = StdMutex::new(BackgroundShellManager::new());
125}
126
127// ==================== WCGW-Style Helper Functions ====================
128
129/// Get WCGW-style status string - matches WCGW Python's `get_status()`
130fn 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        // Add background shell info for main shell - matches WCGW Python
158        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
167/// Process output with WCGW-style incremental text handling - matches WCGW Python _`incremental_text`
168fn 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    // Get text after last pending output
183    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    // Get incremental part - matches WCGW Python get_incremental_output
193    let incremental = get_incremental_output(&last_rendered, &new_rendered);
194    rstrip_lines(&incremental)
195}
196
197/// Right-strip each line and join - matches WCGW Python rstrip
198fn rstrip_lines(lines: &[String]) -> String {
199    lines.iter().map(|line| line.trim_end()).collect::<Vec<_>>().join("\n")
200}
201
202/// Get incremental output between old and new - matches WCGW Python `get_incremental_output`
203fn 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    // Find where old output ends in new output
212    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/// Check if action is effectively a status check - matches WCGW Python `is_status_check`
238#[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 // newline
247        }
248        _ => false,
249    }
250}
251
252// ==================== Main Tool Handler ====================
253
254/// Handles the `BashCommand` tool call with WCGW parity
255///
256/// This function processes the `BashCommand` tool call following WCGW Python's
257/// `execute_bash()` function behavior exactly.
258#[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    // Check if thread_id is empty
268    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    // Extract bash_state data
276    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    // Verify thread ID matches - matches WCGW Python thread_id check
289    if thread_id != bash_state.current_thread_id {
290        // Try to load state from thread_id - matches WCGW Python load_state_from_thread_id
291        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    // Calculate effective timeout - matches WCGW Python
299    // SECURITY: Ensure timeout is not negative to prevent unexpected behavior
300    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    // Execute the action based on type - matches WCGW Python's _execute_bash()
306    let result = execute_bash_action(&mut bash_state, &bash_command.action_json, timeout_s).await;
307
308    // Remove echo if it's a command - matches WCGW Python
309    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
323/// Execute a bash action - matches WCGW Python's _`execute_bash()` function
324async 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    // Handle bg_command_id routing - matches WCGW Python
333    let bg_shell: Option<Arc<StdMutex<Option<InteractiveBash>>>> = match action {
334        BashCommandAction::Command { .. } => None, // Commands don't use bg_command_id for routing
335        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                    // Error message matches WCGW Python
350                    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    // Process based on action type - matches WCGW Python _execute_bash dispatch
364    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
393/// Execute a command - matches WCGW Python's Command handling in _`execute_bash`
394async 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    // Check mode permissions - matches WCGW Python bash_command_mode check
403    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    // Validate single statement - matches WCGW Python assert_single_statement
411    let command = command.trim();
412    crate::utils::bash_parser::assert_single_statement(command)?;
413
414    // If background execution requested, start new shell - matches WCGW Python is_background handling
415    if is_background {
416        return execute_in_background(bash_state, command, timeout_s).await;
417    }
418
419    // Check if a command is already running - matches WCGW Python state check
420    {
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    // Initialize bash if needed
433    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    // Clear prompt before sending - matches WCGW Python clear_to_run
445    // (simplified version - WCGW does more complex clearing)
446
447    // Send command in chunks of 64 characters - matches WCGW Python exactly
448    {
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        // Send in chunks - matches WCGW Python: for i in range(0, len(command), 64)
456        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        // Send linesep to execute - matches WCGW Python bash_state.send(bash_state.linesep, ...)
466        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 with WCGW-style patience handling
484    wait_for_output(bash_state, timeout_s, false, None, false).await
485}
486
487/// Wait for command output with WCGW-style patience handling - matches WCGW Python expect/wait logic
488async 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    // Initial wait - matches WCGW Python wait = min(timeout_s or CONFIG.timeout, CONFIG.timeout_while_output)
501    sleep(Duration::from_secs_f64(wait.min(DEFAULT_TIMEOUT))).await;
502
503    // Read initial output
504    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 not complete and this is a status check, use WCGW-style patience waiting
521    // Matches WCGW Python: if is_status_check(bash_arg) block
522    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            // Check if output changed - matches WCGW Python patience logic
557            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; // Reset patience on new output
562            }
563            last_incremental = new_incremental;
564
565            output = new_output;
566            remaining -= wait;
567        }
568
569        if !complete {
570            // Update pending output - matches WCGW Python bash_state.set_pending(text)
571            last_pending_output = output.clone();
572        }
573    }
574
575    // Process output through terminal emulation - matches WCGW Python _incremental_text
576    let rendered = wcgw_incremental_text(&output, &last_pending_output);
577
578    // Truncate if needed - matches WCGW Python token truncation
579    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    // Calculate running duration for status
586    let running_for = if complete {
587        None
588    } else {
589        Some(format!("{} seconds", (start.elapsed().as_secs() + timeout_s as u64)))
590    };
591
592    // Add status - matches WCGW Python get_status
593    let status = get_status(bash_state, is_bg, bg_id, !complete, running_for.as_deref());
594    Ok(format!("{rendered}{status}"))
595}
596
597/// Execute a status check - matches WCGW Python's `StatusCheck` handling
598async 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    // Check if there's a running command - matches WCGW Python state check
608    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 no command running and not background, return error - matches WCGW Python
620    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    // Read output with patience handling - this IS a status check
630    wait_for_output(bash_state, timeout_s, is_bg, bg_id, true).await
631}
632
633/// Execute `send_text` - matches WCGW Python's `SendText` handling
634async 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    // Validate - matches WCGW Python
645    if text.is_empty() {
646        return Err(WinxError::CommandExecutionError(
647            "Failure: send_text cannot be empty".to_string(),
648        ));
649    }
650
651    // Get the target shell
652    let shell_arc = bg_shell.unwrap_or_else(|| bash_state.interactive_bash.clone());
653
654    // Send text in chunks of 128 characters - matches WCGW Python exactly
655    {
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        // Send in chunks - matches WCGW Python: for i in range(0, len(command_data.send_text), 128)
663        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        // Send linesep - matches WCGW Python bash_state.send(bash_state.linesep, ...)
673        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
685    wait_for_output(bash_state, timeout_s, is_bg, bg_id, false).await
686}
687
688/// Execute `send_specials` - matches WCGW Python's `SendSpecials` handling exactly
689async 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    // Validate - matches WCGW Python
700    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        // Send each special key - matches WCGW Python exactly
717        for key in keys {
718            match key {
719                SpecialKey::KeyUp => {
720                    // matches WCGW Python: bash_state.send("\033[A", ...)
721                    send_bytes_to_bash(bash, b"\x1b[A")?;
722                }
723                SpecialKey::KeyDown => {
724                    // matches WCGW Python: bash_state.send("\033[B", ...)
725                    send_bytes_to_bash(bash, b"\x1b[B")?;
726                }
727                SpecialKey::KeyLeft => {
728                    // matches WCGW Python: bash_state.send("\033[D", ...)
729                    send_bytes_to_bash(bash, b"\x1b[D")?;
730                }
731                SpecialKey::KeyRight => {
732                    // matches WCGW Python: bash_state.send("\033[C", ...)
733                    send_bytes_to_bash(bash, b"\x1b[C")?;
734                }
735                SpecialKey::Enter => {
736                    // matches WCGW Python: bash_state.send("\x0d", ...) - carriage return
737                    send_bytes_to_bash(bash, b"\x0d")?;
738                }
739                SpecialKey::CtrlC => {
740                    // matches WCGW Python: bash_state.sendintr()
741                    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                    // matches WCGW Python: bash_state.sendintr() - same as Ctrl+C in WCGW
748                    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                    // Ctrl+Z = SIGTSTP (suspend) - ASCII 0x1a
755                    send_bytes_to_bash(bash, b"\x1a")?;
756                }
757            }
758        }
759    }
760
761    // Wait for output
762    let mut output = wait_for_output(bash_state, timeout_s, is_bg, bg_id, false).await?;
763
764    // Add interrupt failure message if still running - matches WCGW Python exactly
765    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
772/// Helper to send bytes to bash stdin
773fn 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
788/// Execute `send_ascii` - matches WCGW Python's `SendAscii` handling
789async 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    // Validate - matches WCGW Python
800    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        // Send each ASCII code - matches WCGW Python
817        for &code in ascii_codes {
818            // matches WCGW Python: bash_state.send(chr(ascii_char), ...)
819            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            // Check for interrupt - matches WCGW Python: if ascii_char == 3: is_interrupt = True
830            if code == 3 {
831                is_interrupt = true;
832            }
833        }
834    }
835
836    // Wait for output
837    let mut output = wait_for_output(bash_state, timeout_s, is_bg, bg_id, false).await?;
838
839    // Add interrupt failure message if still running - matches WCGW Python
840    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
847/// Execute command in background - matches WCGW Python's `is_background` handling
848async 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    // Start a new background shell - matches WCGW Python bash_state.start_new_bg_shell
856    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    // Get the shell
867    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    // Send command in chunks - same as regular command
877    {
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    // Wait briefly and get initial output
911    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    // Process output
928    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    // Build status with bg_command_id - matches WCGW Python
936    let status = get_status(bash_state, true, Some(&bg_id), !complete, None);
937
938    // Cleanup if complete - matches WCGW Python
939    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// ==================== Legacy Screen-based Functions (kept for backward compatibility) ====================
950
951/// Process simple command execution for a bash command (legacy)
952#[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            // Fallback: just strip ANSI codes if rendering failed or wasn't needed
978            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/// Execute command in screen (legacy)
1002#[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/// Converts a `SpecialKey` to its screen stuff input representation (legacy)
1061#[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}