Skip to content

Commit 9b47779

Browse files
committed
feat(version): enhance version detection and display across sidecar and binary installations
- Implement regex-based version extraction for more reliable parsing - Add version detection for bundled sidecar installations with proper execution - Improve version checking for sidecar binary with timeout and error handling - Update UI to display version with "v" prefix for consistency - Add comprehensive logging for version detection debugging - Handle edge cases in version extraction with fallback mechanisms
1 parent 6ab32ea commit 9b47779

4 files changed

Lines changed: 194 additions & 29 deletions

File tree

src-tauri/src/claude_binary.rs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -408,15 +408,30 @@ fn get_claude_version(path: &str) -> Result<Option<String>, String> {
408408
/// Extract version string from command output
409409
fn extract_version_from_output(stdout: &[u8]) -> Option<String> {
410410
let output_str = String::from_utf8_lossy(stdout);
411-
412-
// Extract version: first token before whitespace that looks like a version
413-
output_str
414-
.split_whitespace()
415-
.find(|token| {
416-
// Version usually contains dots and numbers
417-
token.chars().any(|c| c == '.') && token.chars().any(|c| c.is_numeric())
418-
})
419-
.map(|s| s.to_string())
411+
412+
// Debug log the raw output
413+
debug!("Raw version output: {:?}", output_str);
414+
415+
// Use regex to directly extract version pattern (e.g., "1.0.41")
416+
// This pattern matches:
417+
// - One or more digits, followed by
418+
// - A dot, followed by
419+
// - One or more digits, followed by
420+
// - A dot, followed by
421+
// - One or more digits
422+
// - Optionally followed by pre-release/build metadata
423+
let version_regex = regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok()?;
424+
425+
if let Some(captures) = version_regex.captures(&output_str) {
426+
if let Some(version_match) = captures.get(1) {
427+
let version = version_match.as_str().to_string();
428+
debug!("Extracted version: {:?}", version);
429+
return Some(version);
430+
}
431+
}
432+
433+
debug!("No version found in output");
434+
None
420435
}
421436

422437
/// Select the best installation based on version

src-tauri/src/commands/agents.rs

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use tauri::{AppHandle, Emitter, Manager, State};
1111
use tauri_plugin_shell::ShellExt;
1212
use tokio::io::{AsyncBufReadExt, BufReader};
1313
use tokio::process::Command;
14+
use regex;
1415

1516
/// Finds the full path to the claude binary
1617
/// This is necessary because macOS apps have a limited PATH environment
@@ -1697,13 +1698,85 @@ pub async fn set_claude_binary_path(db: State<'_, AgentDb>, path: String) -> Res
16971698
/// List all available Claude installations on the system
16981699
#[tauri::command]
16991700
pub async fn list_claude_installations(
1701+
app: AppHandle,
17001702
) -> Result<Vec<crate::claude_binary::ClaudeInstallation>, String> {
1701-
let installations = crate::claude_binary::discover_claude_installations();
1703+
let mut installations = crate::claude_binary::discover_claude_installations();
17021704

17031705
if installations.is_empty() {
17041706
return Err("No Claude Code installations found on the system".to_string());
17051707
}
17061708

1709+
// For bundled installations, execute the sidecar to get the actual version
1710+
for installation in &mut installations {
1711+
if installation.installation_type == crate::claude_binary::InstallationType::Bundled {
1712+
// Try to get the version by executing the sidecar
1713+
use tauri_plugin_shell::process::CommandEvent;
1714+
1715+
// Create a temporary directory for the sidecar to run in
1716+
let temp_dir = std::env::temp_dir();
1717+
1718+
// Create sidecar command with --version flag
1719+
let sidecar_cmd = match app
1720+
.shell()
1721+
.sidecar("claude-code") {
1722+
Ok(cmd) => cmd.args(["--version"]).current_dir(&temp_dir),
1723+
Err(e) => {
1724+
log::warn!("Failed to create sidecar command for version check: {}", e);
1725+
continue;
1726+
}
1727+
};
1728+
1729+
// Spawn the sidecar and collect output
1730+
match sidecar_cmd.spawn() {
1731+
Ok((mut rx, _child)) => {
1732+
let mut stdout_output = String::new();
1733+
let mut stderr_output = String::new();
1734+
1735+
// Set a timeout for version check
1736+
let timeout = tokio::time::Duration::from_secs(5);
1737+
let start_time = tokio::time::Instant::now();
1738+
1739+
while let Ok(Some(event)) = tokio::time::timeout_at(
1740+
start_time + timeout,
1741+
rx.recv()
1742+
).await {
1743+
match event {
1744+
CommandEvent::Stdout(data) => {
1745+
stdout_output.push_str(&String::from_utf8_lossy(&data));
1746+
}
1747+
CommandEvent::Stderr(data) => {
1748+
stderr_output.push_str(&String::from_utf8_lossy(&data));
1749+
}
1750+
CommandEvent::Terminated { .. } => {
1751+
break;
1752+
}
1753+
CommandEvent::Error(e) => {
1754+
log::warn!("Error during sidecar version check: {}", e);
1755+
break;
1756+
}
1757+
_ => {}
1758+
}
1759+
}
1760+
1761+
// Use regex to directly extract version pattern
1762+
let version_regex = regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok();
1763+
1764+
if let Some(regex) = version_regex {
1765+
if let Some(captures) = regex.captures(&stdout_output) {
1766+
if let Some(version_match) = captures.get(1) {
1767+
installation.version = Some(version_match.as_str().to_string());
1768+
log::info!("Bundled sidecar version: {}", version_match.as_str());
1769+
}
1770+
}
1771+
}
1772+
}
1773+
Err(e) => {
1774+
log::warn!("Failed to spawn sidecar for version check: {}", e);
1775+
}
1776+
}
1777+
}
1778+
}
1779+
17071780
Ok(installations)
17081781
}
17091782

src-tauri/src/commands/claude.rs

Lines changed: 95 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use tokio::process::{Child, Command};
1111
use tokio::sync::Mutex;
1212
use tauri_plugin_shell::ShellExt;
1313
use tauri_plugin_shell::process::CommandEvent;
14+
use regex;
1415

1516
/// Global state to track current Claude process
1617
pub struct ClaudeProcessState {
@@ -577,17 +578,89 @@ pub async fn check_claude_version(app: AppHandle) -> Result<ClaudeVersionStatus,
577578
}
578579
};
579580

580-
// If the selected path is the special sidecar identifier, we cannot execute it directly.
581-
// Instead, assume the bundled sidecar is available (find_claude_binary already verified
582-
// this) and return a positive status without a version string. Attempting to spawn the
583-
// sidecar here would require async streaming plumbing that is over-kill for a simple
584-
// presence check and fails in debug builds (os error 2).
581+
// If the selected path is the special sidecar identifier, execute it to get version
585582
if claude_path == "claude-code" {
586-
return Ok(ClaudeVersionStatus {
587-
is_installed: true,
588-
version: None,
589-
output: "Using bundled Claude Code sidecar".to_string(),
590-
});
583+
use tauri_plugin_shell::process::CommandEvent;
584+
585+
// Create a temporary directory for the sidecar to run in
586+
let temp_dir = std::env::temp_dir();
587+
588+
// Create sidecar command with --version flag
589+
let sidecar_cmd = match app
590+
.shell()
591+
.sidecar("claude-code") {
592+
Ok(cmd) => cmd.args(["--version"]).current_dir(&temp_dir),
593+
Err(e) => {
594+
log::error!("Failed to create sidecar command: {}", e);
595+
return Ok(ClaudeVersionStatus {
596+
is_installed: true, // We know it exists, just couldn't create command
597+
version: None,
598+
output: format!("Using bundled Claude Code sidecar (command creation failed: {})", e),
599+
});
600+
}
601+
};
602+
603+
// Spawn the sidecar and collect output
604+
match sidecar_cmd.spawn() {
605+
Ok((mut rx, _child)) => {
606+
let mut stdout_output = String::new();
607+
let mut stderr_output = String::new();
608+
let mut exit_success = false;
609+
610+
// Collect output from the sidecar
611+
while let Some(event) = rx.recv().await {
612+
match event {
613+
CommandEvent::Stdout(data) => {
614+
let line = String::from_utf8_lossy(&data);
615+
stdout_output.push_str(&line);
616+
}
617+
CommandEvent::Stderr(data) => {
618+
let line = String::from_utf8_lossy(&data);
619+
stderr_output.push_str(&line);
620+
}
621+
CommandEvent::Terminated(payload) => {
622+
exit_success = payload.code.unwrap_or(-1) == 0;
623+
break;
624+
}
625+
_ => {}
626+
}
627+
}
628+
629+
// Use regex to directly extract version pattern (e.g., "1.0.41")
630+
let version_regex = regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok();
631+
632+
let version = if let Some(regex) = version_regex {
633+
regex.captures(&stdout_output)
634+
.and_then(|captures| captures.get(1))
635+
.map(|m| m.as_str().to_string())
636+
} else {
637+
None
638+
};
639+
640+
let full_output = if stderr_output.is_empty() {
641+
stdout_output.clone()
642+
} else {
643+
format!("{}\n{}", stdout_output, stderr_output)
644+
};
645+
646+
// Check if the output matches the expected format
647+
let is_valid = stdout_output.contains("(Claude Code)") || stdout_output.contains("Claude Code") || version.is_some();
648+
649+
return Ok(ClaudeVersionStatus {
650+
is_installed: is_valid && exit_success,
651+
version,
652+
output: full_output.trim().to_string(),
653+
});
654+
}
655+
Err(e) => {
656+
log::error!("Failed to execute sidecar: {}", e);
657+
return Ok(ClaudeVersionStatus {
658+
is_installed: true, // We know it exists, just couldn't get version
659+
version: None,
660+
output: format!("Using bundled Claude Code sidecar (version check failed: {})", e),
661+
});
662+
}
663+
}
591664
}
592665

593666
use log::debug;debug!("Claude path: {}", claude_path);
@@ -622,6 +695,18 @@ pub async fn check_claude_version(app: AppHandle) -> Result<ClaudeVersionStatus,
622695
Ok(output) => {
623696
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
624697
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
698+
699+
// Use regex to directly extract version pattern (e.g., "1.0.41")
700+
let version_regex = regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok();
701+
702+
let version = if let Some(regex) = version_regex {
703+
regex.captures(&stdout)
704+
.and_then(|captures| captures.get(1))
705+
.map(|m| m.as_str().to_string())
706+
} else {
707+
None
708+
};
709+
625710
let full_output = if stderr.is_empty() {
626711
stdout.clone()
627712
} else {
@@ -632,14 +717,6 @@ pub async fn check_claude_version(app: AppHandle) -> Result<ClaudeVersionStatus,
632717
// Expected format: "1.0.17 (Claude Code)" or similar
633718
let is_valid = stdout.contains("(Claude Code)") || stdout.contains("Claude Code");
634719

635-
// Extract version number if valid
636-
let version = if is_valid {
637-
// Try to extract just the version number
638-
stdout.split_whitespace().next().map(|s| s.to_string())
639-
} else {
640-
None
641-
};
642-
643720
Ok(ClaudeVersionStatus {
644721
is_installed: is_valid && output.status.success(),
645722
version,

src/components/Topbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export const Topbar: React.FC<TopbarProps> = ({
112112
/>
113113
<span>
114114
{versionStatus.is_installed && versionStatus.version
115-
? `Claude Code ${versionStatus.version}`
115+
? `Claude Code v${versionStatus.version}`
116116
: "Claude Code"}
117117
</span>
118118
</div>

0 commit comments

Comments
 (0)