修复导航栏claude版本不显示
Some checks failed
Build Linux / Build Linux x86_64 (push) Has been cancelled
Build Test / Build Test (Linux) (push) Has been cancelled
Build Test / Build Test (Windows) (push) Has been cancelled
Build Test / Build Test (macOS) (push) Has been cancelled
Build Test / Build Test Summary (push) Has been cancelled

This commit is contained in:
2025-09-16 17:14:05 +08:00
parent 4cdb22788f
commit 564c9d77f6
3 changed files with 151 additions and 107 deletions

View File

@@ -202,7 +202,16 @@ fn find_which_installations() -> Vec<ClaudeInstallation> {
let mut installations = Vec::new();
match Command::new(command_name).arg("claude").output() {
// Create command with enhanced PATH for production environments
let mut cmd = Command::new(command_name);
cmd.arg("claude");
// In production (DMG), we need to ensure proper PATH is set
let enhanced_path = build_enhanced_path();
debug!("Using enhanced PATH for {}: {}", command_name, enhanced_path);
cmd.env("PATH", enhanced_path);
match cmd.output() {
Ok(output) if output.status.success() => {
let output_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
@@ -401,7 +410,11 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
}
// Also check if claude is available in PATH (without full path)
if let Ok(output) = Command::new("claude").arg("--version").output() {
let mut path_cmd = Command::new("claude");
path_cmd.arg("--version");
path_cmd.env("PATH", build_enhanced_path());
if let Ok(output) = path_cmd.output() {
if output.status.success() {
debug!("claude is available in PATH");
// Combine stdout and stderr for robust version extraction
@@ -427,7 +440,11 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
/// Get Claude version by running --version command
fn get_claude_version(path: &str) -> Result<Option<String>, String> {
match Command::new(path).arg("--version").output() {
// Use the helper function to create command with proper environment
let mut cmd = create_command_with_env(path);
cmd.arg("--version");
match cmd.output() {
Ok(output) => {
if output.status.success() {
// Combine stdout and stderr for robust version extraction
@@ -556,11 +573,15 @@ pub fn create_command_with_env(program: &str) -> Command {
info!("Creating command for: {}", program);
// Build enhanced PATH for production environments (DMG/App Bundle)
let enhanced_path = build_enhanced_path();
debug!("Enhanced PATH: {}", enhanced_path);
cmd.env("PATH", enhanced_path.clone());
// Inherit essential environment variables from parent process
for (key, value) in std::env::vars() {
// Pass through PATH and other essential environment variables
if key == "PATH"
|| key == "HOME"
// Pass through essential environment variables (excluding PATH which we set above)
if key == "HOME"
|| key == "USER"
|| key == "SHELL"
|| key == "LANG"
@@ -595,7 +616,12 @@ pub fn create_command_with_env(program: &str) -> Command {
if program.contains("/.nvm/versions/node/") {
if let Some(node_bin_dir) = std::path::Path::new(program).parent() {
// Ensure the Node.js bin directory is in PATH
let current_path = std::env::var("PATH").unwrap_or_default();
let current_path = cmd.get_envs()
.find(|(k, _)| k.to_str() == Some("PATH"))
.and_then(|(_, v)| v)
.and_then(|v| v.to_str())
.unwrap_or(&enhanced_path)
.to_string();
let node_bin_str = node_bin_dir.to_string_lossy();
if !current_path.contains(&node_bin_str.as_ref()) {
let new_path = format!("{}:{}", node_bin_str, current_path);
@@ -607,3 +633,73 @@ pub fn create_command_with_env(program: &str) -> Command {
cmd
}
/// Build an enhanced PATH that includes all possible Claude installation locations
/// This is especially important for DMG/packaged applications where PATH may be limited
fn build_enhanced_path() -> String {
let mut paths = Vec::new();
// Start with current PATH
if let Ok(current_path) = std::env::var("PATH") {
paths.push(current_path);
}
// Add standard system paths that might be missing in packaged apps
let system_paths = vec![
"/usr/local/bin",
"/usr/bin",
"/bin",
"/opt/homebrew/bin",
"/opt/homebrew/sbin",
];
for path in system_paths {
if PathBuf::from(path).exists() {
paths.push(path.to_string());
}
}
// Add user-specific paths
if let Ok(home) = std::env::var("HOME") {
let user_paths = vec![
format!("{}/.local/bin", home),
format!("{}/.claude/local", home),
format!("{}/.npm-global/bin", home),
format!("{}/.yarn/bin", home),
format!("{}/.bun/bin", home),
format!("{}/bin", home),
format!("{}/.config/yarn/global/node_modules/.bin", home),
format!("{}/node_modules/.bin", home),
];
for path in user_paths {
if PathBuf::from(&path).exists() {
paths.push(path);
}
}
// Add all NVM node versions
let nvm_dir = PathBuf::from(&home).join(".nvm/versions/node");
if nvm_dir.exists() {
if let Ok(entries) = std::fs::read_dir(&nvm_dir) {
for entry in entries.flatten() {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
let bin_path = entry.path().join("bin");
if bin_path.exists() {
paths.push(bin_path.to_string_lossy().to_string());
}
}
}
}
}
}
// Remove duplicates while preserving order
let mut seen = std::collections::HashSet::new();
let unique_paths: Vec<String> = paths
.into_iter()
.filter(|path| seen.insert(path.clone()))
.collect();
unique_paths.join(":")
}

View File

@@ -623,102 +623,40 @@ pub async fn get_system_prompt() -> Result<String, String> {
/// Checks if Claude Code is installed and gets its version
#[tauri::command]
pub async fn check_claude_version(app: AppHandle) -> Result<ClaudeVersionStatus, String> {
pub async fn check_claude_version(_app: AppHandle) -> Result<ClaudeVersionStatus, String> {
log::info!("Checking Claude Code version");
let claude_path = match find_claude_binary(&app) {
Ok(path) => path,
Err(e) => {
return Ok(ClaudeVersionStatus {
is_installed: false,
version: None,
output: e,
});
}
};
use log::debug;debug!("Claude path: {}", claude_path);
// In production builds, we can't check the version directly
#[cfg(not(debug_assertions))]
{
log::warn!("Cannot check claude version in production build");
// If we found a path (either stored or in common locations), assume it's installed
if claude_path != "claude" && PathBuf::from(&claude_path).exists() {
return Ok(ClaudeVersionStatus {
is_installed: true,
version: None,
output: "Claude binary found at: ".to_string() + &claude_path,
});
} else {
return Ok(ClaudeVersionStatus {
is_installed: false,
version: None,
output: "Cannot verify Claude installation in production build. Please ensure Claude Code is installed.".to_string(),
});
}
// Try to find Claude installations with versions
let installations = crate::claude_binary::discover_claude_installations();
if installations.is_empty() {
return Ok(ClaudeVersionStatus {
is_installed: false,
version: None,
output: "Claude Code not found. Please ensure it's installed.".to_string(),
});
}
#[cfg(debug_assertions)]
{
let output = std::process::Command::new(claude_path)
.arg("--version")
.output();
match output {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
// Use regex to directly extract version pattern (e.g., "1.0.41")
let version_regex = regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok();
// Combine stdout and stderr for version extraction (some tools write version to stderr)
let mut version_src = stdout.clone();
if !stderr.is_empty() {
version_src.push('\n');
version_src.push_str(&stderr);
}
let version = if let Some(regex) = version_regex {
regex
.captures(&version_src)
.and_then(|captures| captures.get(1))
.map(|m| m.as_str().to_string())
} else {
None
};
let full_output = if stderr.is_empty() {
stdout.clone()
} else {
format!("{}\n{}", stdout, stderr)
};
// Check if the output matches the expected format
// Expected format: "1.0.17 (Claude Code)" or similar
let is_valid =
stdout.contains("(Claude Code)")
|| stdout.contains("Claude Code")
|| stderr.contains("(Claude Code)")
|| stderr.contains("Claude Code");
Ok(ClaudeVersionStatus {
is_installed: is_valid && output.status.success(),
version,
output: full_output.trim().to_string(),
})
// Find the best installation (highest version or first found)
let best_installation = installations
.into_iter()
.max_by(|a, b| {
match (&a.version, &b.version) {
(Some(v1), Some(v2)) => v1.cmp(v2),
(Some(_), None) => std::cmp::Ordering::Greater,
(None, Some(_)) => std::cmp::Ordering::Less,
(None, None) => std::cmp::Ordering::Equal,
}
Err(e) => {
log::error!("Failed to run claude command: {}", e);
Ok(ClaudeVersionStatus {
is_installed: false,
version: None,
output: format!("Command not found: {}", e),
})
}
}
}
})
.unwrap(); // Safe because we checked is_empty() above
log::info!("Found Claude installation: {:?}", best_installation);
Ok(ClaudeVersionStatus {
is_installed: true,
version: best_installation.version,
output: format!("Claude binary found at: {}", best_installation.path),
})
}
/// Saves the CLAUDE.md system prompt file

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, lazy, Suspense } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Plus, Loader2, ArrowLeft } from "lucide-react";
import { api, type Project, type Session, type ClaudeMdFile } from "@/lib/api";
@@ -10,12 +10,8 @@ import { ProjectList } from "@/components/ProjectList";
import { SessionList } from "@/components/SessionList";
import { RunningClaudeSessions } from "@/components/RunningClaudeSessions";
import { Topbar } from "@/components/Topbar";
import { MarkdownEditor } from "@/components/MarkdownEditor";
import { ClaudeFileEditor } from "@/components/ClaudeFileEditor";
import { Settings } from "@/components/Settings";
import { CCAgents } from "@/components/CCAgents";
import { UsageDashboard } from "@/components/UsageDashboard";
import { MCPManager } from "@/components/MCPManager";
import { NFOCredits } from "@/components/NFOCredits";
import { ClaudeBinaryDialog } from "@/components/ClaudeBinaryDialog";
import { Toast, ToastContainer } from "@/components/ui/toast";
@@ -32,6 +28,12 @@ import RelayStationManager from "@/components/RelayStationManager";
import { CcrRouterManager } from "@/components/CcrRouterManager";
import i18n from "@/lib/i18n";
// Lazy load these components to match TabContent's dynamic imports
const MarkdownEditor = lazy(() => import('@/components/MarkdownEditor').then(m => ({ default: m.MarkdownEditor })));
const Settings = lazy(() => import('@/components/Settings').then(m => ({ default: m.Settings })));
const UsageDashboard = lazy(() => import('@/components/UsageDashboard').then(m => ({ default: m.UsageDashboard })));
const MCPManager = lazy(() => import('@/components/MCPManager').then(m => ({ default: m.MCPManager })));
type View =
| "welcome"
| "projects"
@@ -299,14 +301,18 @@ function AppContent() {
case "editor":
return (
<div className="flex-1 overflow-hidden">
<MarkdownEditor onBack={() => handleViewChange("welcome")} />
<Suspense fallback={<div className="flex items-center justify-center h-full"><Loader2 className="h-6 w-6 animate-spin" /></div>}>
<MarkdownEditor onBack={() => handleViewChange("welcome")} />
</Suspense>
</div>
);
case "settings":
return (
<div className="flex-1 flex flex-col" style={{ minHeight: 0 }}>
<Settings onBack={() => handleViewChange("welcome")} />
<Suspense fallback={<div className="flex items-center justify-center h-full"><Loader2 className="h-6 w-6 animate-spin" /></div>}>
<Settings onBack={() => handleViewChange("welcome")} />
</Suspense>
</div>
);
@@ -459,12 +465,16 @@ function AppContent() {
case "usage-dashboard":
return (
<UsageDashboard onBack={() => handleViewChange("welcome")} />
<Suspense fallback={<div className="flex items-center justify-center h-full"><Loader2 className="h-6 w-6 animate-spin" /></div>}>
<UsageDashboard onBack={() => handleViewChange("welcome")} />
</Suspense>
);
case "mcp":
return (
<MCPManager onBack={() => handleViewChange("welcome")} />
<Suspense fallback={<div className="flex items-center justify-center h-full"><Loader2 className="h-6 w-6 animate-spin" /></div>}>
<MCPManager onBack={() => handleViewChange("welcome")} />
</Suspense>
);
case "project-settings":