Skip to content

Commit bc6c10d

Browse files
authored
feat(windows): fix interface issues and command execution (#136)
* feat(windows): refactor command execute * feat(windows): update tool manager to support windows * fix(windows): fix prompt input issues * fix: clippy issues * fix(windows): fix non-exe mcp spawning * test(windows): add command parsing tests * fix(windows): fixes for clippy and tests * fix(windows): fix transport test * fix(windows): clippy fixes
1 parent ffef84d commit bc6c10d

File tree

16 files changed

+1167
-524
lines changed

16 files changed

+1167
-524
lines changed

crates/chat-cli/src/cli/chat/hooks.rs

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,13 +295,24 @@ impl HookExecutor {
295295
async fn execute_inline_hook(&self, hook: &Hook) -> Result<String> {
296296
let command = hook.command.as_ref().ok_or_else(|| eyre!("no command specified"))?;
297297

298+
#[cfg(unix)]
298299
let command_future = tokio::process::Command::new("bash")
299300
.arg("-c")
300301
.arg(command)
301302
.stdin(Stdio::piped())
302303
.stdout(Stdio::piped())
303304
.stderr(Stdio::piped())
304305
.output();
306+
307+
#[cfg(windows)]
308+
let command_future = tokio::process::Command::new("cmd")
309+
.arg("/C")
310+
.arg(command)
311+
.stdin(Stdio::piped())
312+
.stdout(Stdio::piped())
313+
.stderr(Stdio::piped())
314+
.output();
315+
305316
let timeout = Duration::from_millis(hook.timeout_ms);
306317

307318
// Run with timeout
@@ -544,14 +555,47 @@ mod tests {
544555
#[tokio::test]
545556
async fn test_max_output_size() {
546557
let mut executor = HookExecutor::new();
547-
let mut hook = Hook::new_inline_hook(
548-
HookTrigger::PerPrompt,
549-
"for i in {1..1000}; do echo $i; done".to_string(),
550-
);
558+
559+
// Use different commands based on OS
560+
#[cfg(unix)]
561+
let command = "for i in {1..1000}; do echo $i; done";
562+
563+
#[cfg(windows)]
564+
let command = "for /L %i in (1,1,1000) do @echo %i";
565+
566+
let mut hook = Hook::new_inline_hook(HookTrigger::PerPrompt, command.to_string());
551567
hook.max_output_size = 100;
552568

553569
let results = executor.run_hooks(vec![&hook], None::<&mut Stdout>).await;
554570

555571
assert!(results[0].1.len() <= hook.max_output_size + " ... truncated".len());
556572
}
573+
574+
#[tokio::test]
575+
async fn test_os_specific_command_execution() {
576+
let mut executor = HookExecutor::new();
577+
578+
// Create a simple command that outputs the shell name
579+
#[cfg(unix)]
580+
let command = "echo $SHELL";
581+
582+
#[cfg(windows)]
583+
let command = "echo %ComSpec%";
584+
585+
let hook = Hook::new_inline_hook(HookTrigger::PerPrompt, command.to_string());
586+
587+
let results = executor.run_hooks(vec![&hook], None::<&mut Stdout>).await;
588+
589+
assert_eq!(results.len(), 1, "Command execution should succeed");
590+
591+
// Verify output contains expected shell information
592+
#[cfg(unix)]
593+
assert!(results[0].1.contains("/"), "Unix shell path should contain '/'");
594+
595+
#[cfg(windows)]
596+
assert!(
597+
results[0].1.to_lowercase().contains("cmd.exe") || results[0].1.to_lowercase().contains("command.com"),
598+
"Windows shell path should contain cmd.exe or command.com"
599+
);
600+
}
557601
}

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod message;
88
mod parse;
99
mod parser;
1010
mod prompt;
11+
mod prompt_parser;
1112
mod server_messenger;
1213
#[cfg(unix)]
1314
mod skim_integration;
@@ -1378,7 +1379,7 @@ impl ChatContext {
13781379
/// Read input from the user.
13791380
async fn prompt_user(
13801381
&mut self,
1381-
database: &Database,
1382+
#[cfg_attr(windows, allow(unused_variables))] database: &Database,
13821383
mut tool_uses: Option<Vec<QueuedTool>>,
13831384
pending_tool_index: Option<usize>,
13841385
skip_printing_tools: bool,
@@ -1532,8 +1533,36 @@ impl ChatContext {
15321533
},
15331534
Command::Execute { command } => {
15341535
queue!(self.output, style::Print('\n'))?;
1535-
std::process::Command::new("bash").args(["-c", &command]).status().ok();
1536-
queue!(self.output, style::Print('\n'))?;
1536+
1537+
// Use platform-appropriate shell
1538+
let result = if cfg!(target_os = "windows") {
1539+
std::process::Command::new("cmd").args(["/C", &command]).status()
1540+
} else {
1541+
std::process::Command::new("bash").args(["-c", &command]).status()
1542+
};
1543+
1544+
// Handle the result and provide appropriate feedback
1545+
match result {
1546+
Ok(status) => {
1547+
if !status.success() {
1548+
queue!(
1549+
self.output,
1550+
style::SetForegroundColor(Color::Yellow),
1551+
style::Print(format!("Command exited with status: {}\n", status)),
1552+
style::SetForegroundColor(Color::Reset)
1553+
)?;
1554+
}
1555+
},
1556+
Err(e) => {
1557+
queue!(
1558+
self.output,
1559+
style::SetForegroundColor(Color::Red),
1560+
style::Print(format!("Failed to execute command: {}\n", e)),
1561+
style::SetForegroundColor(Color::Reset)
1562+
)?;
1563+
},
1564+
}
1565+
15371566
ChatState::PromptUser {
15381567
tool_uses: None,
15391568
pending_tool_index: None,

crates/chat-cli/src/cli/chat/prompt.rs

Lines changed: 116 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use std::borrow::Cow;
22

3-
use crossterm::style::Stylize;
43
use eyre::Result;
54
use rustyline::completion::{
65
Completer,
@@ -35,6 +34,8 @@ use rustyline::{
3534
};
3635
use winnow::stream::AsChar;
3736

37+
pub use super::prompt_parser::generate_prompt;
38+
use super::prompt_parser::parse_prompt_components;
3839
use crate::database::Database;
3940
use crate::database::settings::Setting;
4041

@@ -81,16 +82,6 @@ pub const COMMANDS: &[&str] = &[
8182
"/load",
8283
];
8384

84-
pub fn generate_prompt(current_profile: Option<&str>, warning: bool) -> String {
85-
let warning_symbol = if warning { "!".red().to_string() } else { "".to_string() };
86-
let profile_part = current_profile
87-
.filter(|&p| p != "default")
88-
.map(|p| format!("[{p}] ").cyan().to_string())
89-
.unwrap_or_default();
90-
91-
format!("{profile_part}{warning_symbol}{}", "> ".magenta())
92-
}
93-
9485
/// Complete commands that start with a slash
9586
fn complete_command(word: &str, start: usize) -> (usize, Vec<String>) {
9687
(
@@ -265,6 +256,34 @@ impl Highlighter for ChatHelper {
265256
fn highlight_char(&self, _line: &str, _pos: usize, _kind: CmdKind) -> bool {
266257
false
267258
}
259+
260+
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(&'s self, prompt: &'p str, _default: bool) -> Cow<'b, str> {
261+
use crossterm::style::Stylize;
262+
263+
// Parse the plain text prompt to extract profile and warning information
264+
// and apply colors using crossterm's ANSI escape codes
265+
if let Some(components) = parse_prompt_components(prompt) {
266+
let mut result = String::new();
267+
268+
// Add profile part if present
269+
if let Some(profile) = components.profile {
270+
result.push_str(&format!("[{}] ", profile).cyan().to_string());
271+
}
272+
273+
// Add warning symbol if present
274+
if components.warning {
275+
result.push_str(&"!".red().to_string());
276+
}
277+
278+
// Add the prompt symbol
279+
result.push_str(&"> ".magenta().to_string());
280+
281+
Cow::Owned(result)
282+
} else {
283+
// If we can't parse the prompt, return it as-is
284+
Cow::Borrowed(prompt)
285+
}
286+
}
268287
}
269288

270289
pub fn rl(
@@ -306,28 +325,10 @@ pub fn rl(
306325

307326
#[cfg(test)]
308327
mod tests {
309-
use super::*;
310-
311-
#[test]
312-
fn test_generate_prompt() {
313-
// Test default prompt (no profile)
314-
assert_eq!(generate_prompt(None, false), "> ".magenta().to_string());
315-
// Test default prompt with warning
316-
assert_eq!(generate_prompt(None, true), format!("{}{}", "!".red(), "> ".magenta()));
317-
// Test default profile (should be same as no profile)
318-
assert_eq!(generate_prompt(Some("default"), false), "> ".magenta().to_string());
319-
// Test custom profile
320-
assert_eq!(
321-
generate_prompt(Some("test-profile"), false),
322-
format!("{}{}", "[test-profile] ".cyan(), "> ".magenta())
323-
);
324-
// Test another custom profile with warning
325-
assert_eq!(
326-
generate_prompt(Some("dev"), true),
327-
format!("{}{}{}", "[dev] ".cyan(), "!".red(), "> ".magenta())
328-
);
329-
}
328+
use crossterm::style::Stylize;
329+
use rustyline::highlight::Highlighter;
330330

331+
use super::*;
331332
#[test]
332333
fn test_chat_completer_command_completion() {
333334
let (prompt_request_sender, _) = std::sync::mpsc::channel::<Option<String>>();
@@ -368,4 +369,87 @@ mod tests {
368369
// Verify no completions are returned for regular text
369370
assert!(completions.is_empty());
370371
}
372+
373+
#[test]
374+
fn test_highlight_prompt_basic() {
375+
let (prompt_request_sender, _) = std::sync::mpsc::channel::<Option<String>>();
376+
let (_, prompt_response_receiver) = std::sync::mpsc::channel::<Vec<String>>();
377+
let helper = ChatHelper {
378+
completer: ChatCompleter::new(prompt_request_sender, prompt_response_receiver),
379+
hinter: (),
380+
validator: MultiLineValidator,
381+
};
382+
383+
// Test basic prompt highlighting
384+
let highlighted = helper.highlight_prompt("> ", true);
385+
386+
assert_eq!(highlighted, "> ".magenta().to_string());
387+
}
388+
389+
#[test]
390+
fn test_highlight_prompt_with_warning() {
391+
let (prompt_request_sender, _) = std::sync::mpsc::channel::<Option<String>>();
392+
let (_, prompt_response_receiver) = std::sync::mpsc::channel::<Vec<String>>();
393+
let helper = ChatHelper {
394+
completer: ChatCompleter::new(prompt_request_sender, prompt_response_receiver),
395+
hinter: (),
396+
validator: MultiLineValidator,
397+
};
398+
399+
// Test warning prompt highlighting
400+
let highlighted = helper.highlight_prompt("!> ", true);
401+
402+
assert_eq!(highlighted, format!("{}{}", "!".red(), "> ".magenta()));
403+
}
404+
405+
#[test]
406+
fn test_highlight_prompt_with_profile() {
407+
let (prompt_request_sender, _) = std::sync::mpsc::channel::<Option<String>>();
408+
let (_, prompt_response_receiver) = std::sync::mpsc::channel::<Vec<String>>();
409+
let helper = ChatHelper {
410+
completer: ChatCompleter::new(prompt_request_sender, prompt_response_receiver),
411+
hinter: (),
412+
validator: MultiLineValidator,
413+
};
414+
415+
// Test profile prompt highlighting
416+
let highlighted = helper.highlight_prompt("[test-profile] > ", true);
417+
418+
assert_eq!(highlighted, format!("{}{}", "[test-profile] ".cyan(), "> ".magenta()));
419+
}
420+
421+
#[test]
422+
fn test_highlight_prompt_with_profile_and_warning() {
423+
let (prompt_request_sender, _) = std::sync::mpsc::channel::<Option<String>>();
424+
let (_, prompt_response_receiver) = std::sync::mpsc::channel::<Vec<String>>();
425+
let helper = ChatHelper {
426+
completer: ChatCompleter::new(prompt_request_sender, prompt_response_receiver),
427+
hinter: (),
428+
validator: MultiLineValidator,
429+
};
430+
431+
// Test profile + warning prompt highlighting
432+
let highlighted = helper.highlight_prompt("[dev] !> ", true);
433+
// Should have cyan profile + red warning + cyan bold prompt
434+
assert_eq!(
435+
highlighted,
436+
format!("{}{}{}", "[dev] ".cyan(), "!".red(), "> ".magenta())
437+
);
438+
}
439+
440+
#[test]
441+
fn test_highlight_prompt_invalid_format() {
442+
let (prompt_request_sender, _) = std::sync::mpsc::channel::<Option<String>>();
443+
let (_, prompt_response_receiver) = std::sync::mpsc::channel::<Vec<String>>();
444+
let helper = ChatHelper {
445+
completer: ChatCompleter::new(prompt_request_sender, prompt_response_receiver),
446+
hinter: (),
447+
validator: MultiLineValidator,
448+
};
449+
450+
// Test invalid prompt format (should return as-is)
451+
let invalid_prompt = "invalid prompt format";
452+
let highlighted = helper.highlight_prompt(invalid_prompt, true);
453+
assert_eq!(highlighted, invalid_prompt);
454+
}
371455
}

0 commit comments

Comments
 (0)