修复导航栏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
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:
@@ -202,7 +202,16 @@ fn find_which_installations() -> Vec<ClaudeInstallation> {
|
|||||||
|
|
||||||
let mut installations = Vec::new();
|
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() => {
|
Ok(output) if output.status.success() => {
|
||||||
let output_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
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)
|
// 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() {
|
if output.status.success() {
|
||||||
debug!("claude is available in PATH");
|
debug!("claude is available in PATH");
|
||||||
// Combine stdout and stderr for robust version extraction
|
// 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
|
/// Get Claude version by running --version command
|
||||||
fn get_claude_version(path: &str) -> Result<Option<String>, String> {
|
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) => {
|
Ok(output) => {
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
// Combine stdout and stderr for robust version extraction
|
// 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);
|
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
|
// Inherit essential environment variables from parent process
|
||||||
for (key, value) in std::env::vars() {
|
for (key, value) in std::env::vars() {
|
||||||
// Pass through PATH and other essential environment variables
|
// Pass through essential environment variables (excluding PATH which we set above)
|
||||||
if key == "PATH"
|
if key == "HOME"
|
||||||
|| key == "HOME"
|
|
||||||
|| key == "USER"
|
|| key == "USER"
|
||||||
|| key == "SHELL"
|
|| key == "SHELL"
|
||||||
|| key == "LANG"
|
|| key == "LANG"
|
||||||
@@ -595,7 +616,12 @@ pub fn create_command_with_env(program: &str) -> Command {
|
|||||||
if program.contains("/.nvm/versions/node/") {
|
if program.contains("/.nvm/versions/node/") {
|
||||||
if let Some(node_bin_dir) = std::path::Path::new(program).parent() {
|
if let Some(node_bin_dir) = std::path::Path::new(program).parent() {
|
||||||
// Ensure the Node.js bin directory is in PATH
|
// 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();
|
let node_bin_str = node_bin_dir.to_string_lossy();
|
||||||
if !current_path.contains(&node_bin_str.as_ref()) {
|
if !current_path.contains(&node_bin_str.as_ref()) {
|
||||||
let new_path = format!("{}:{}", node_bin_str, current_path);
|
let new_path = format!("{}:{}", node_bin_str, current_path);
|
||||||
@@ -607,3 +633,73 @@ pub fn create_command_with_env(program: &str) -> Command {
|
|||||||
|
|
||||||
cmd
|
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(":")
|
||||||
|
}
|
||||||
|
@@ -623,102 +623,40 @@ pub async fn get_system_prompt() -> Result<String, String> {
|
|||||||
|
|
||||||
/// Checks if Claude Code is installed and gets its version
|
/// Checks if Claude Code is installed and gets its version
|
||||||
#[tauri::command]
|
#[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");
|
log::info!("Checking Claude Code version");
|
||||||
|
|
||||||
let claude_path = match find_claude_binary(&app) {
|
// Try to find Claude installations with versions
|
||||||
Ok(path) => path,
|
let installations = crate::claude_binary::discover_claude_installations();
|
||||||
Err(e) => {
|
|
||||||
return Ok(ClaudeVersionStatus {
|
if installations.is_empty() {
|
||||||
is_installed: false,
|
return Ok(ClaudeVersionStatus {
|
||||||
version: None,
|
is_installed: false,
|
||||||
output: e,
|
version: None,
|
||||||
});
|
output: "Claude Code not found. Please ensure it's installed.".to_string(),
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
|
||||||
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(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
// Find the best installation (highest version or first found)
|
||||||
{
|
let best_installation = installations
|
||||||
let output = std::process::Command::new(claude_path)
|
.into_iter()
|
||||||
.arg("--version")
|
.max_by(|a, b| {
|
||||||
.output();
|
match (&a.version, &b.version) {
|
||||||
|
(Some(v1), Some(v2)) => v1.cmp(v2),
|
||||||
match output {
|
(Some(_), None) => std::cmp::Ordering::Greater,
|
||||||
Ok(output) => {
|
(None, Some(_)) => std::cmp::Ordering::Less,
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
(None, None) => std::cmp::Ordering::Equal,
|
||||||
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(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
})
|
||||||
log::error!("Failed to run claude command: {}", e);
|
.unwrap(); // Safe because we checked is_empty() above
|
||||||
Ok(ClaudeVersionStatus {
|
|
||||||
is_installed: false,
|
log::info!("Found Claude installation: {:?}", best_installation);
|
||||||
version: None,
|
|
||||||
output: format!("Command not found: {}", e),
|
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
|
/// Saves the CLAUDE.md system prompt file
|
||||||
|
28
src/App.tsx
28
src/App.tsx
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, lazy, Suspense } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { Plus, Loader2, ArrowLeft } from "lucide-react";
|
import { Plus, Loader2, ArrowLeft } from "lucide-react";
|
||||||
import { api, type Project, type Session, type ClaudeMdFile } from "@/lib/api";
|
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 { SessionList } from "@/components/SessionList";
|
||||||
import { RunningClaudeSessions } from "@/components/RunningClaudeSessions";
|
import { RunningClaudeSessions } from "@/components/RunningClaudeSessions";
|
||||||
import { Topbar } from "@/components/Topbar";
|
import { Topbar } from "@/components/Topbar";
|
||||||
import { MarkdownEditor } from "@/components/MarkdownEditor";
|
|
||||||
import { ClaudeFileEditor } from "@/components/ClaudeFileEditor";
|
import { ClaudeFileEditor } from "@/components/ClaudeFileEditor";
|
||||||
import { Settings } from "@/components/Settings";
|
|
||||||
import { CCAgents } from "@/components/CCAgents";
|
import { CCAgents } from "@/components/CCAgents";
|
||||||
import { UsageDashboard } from "@/components/UsageDashboard";
|
|
||||||
import { MCPManager } from "@/components/MCPManager";
|
|
||||||
import { NFOCredits } from "@/components/NFOCredits";
|
import { NFOCredits } from "@/components/NFOCredits";
|
||||||
import { ClaudeBinaryDialog } from "@/components/ClaudeBinaryDialog";
|
import { ClaudeBinaryDialog } from "@/components/ClaudeBinaryDialog";
|
||||||
import { Toast, ToastContainer } from "@/components/ui/toast";
|
import { Toast, ToastContainer } from "@/components/ui/toast";
|
||||||
@@ -32,6 +28,12 @@ import RelayStationManager from "@/components/RelayStationManager";
|
|||||||
import { CcrRouterManager } from "@/components/CcrRouterManager";
|
import { CcrRouterManager } from "@/components/CcrRouterManager";
|
||||||
import i18n from "@/lib/i18n";
|
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 =
|
type View =
|
||||||
| "welcome"
|
| "welcome"
|
||||||
| "projects"
|
| "projects"
|
||||||
@@ -299,14 +301,18 @@ function AppContent() {
|
|||||||
case "editor":
|
case "editor":
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-hidden">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "settings":
|
case "settings":
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex flex-col" style={{ minHeight: 0 }}>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -459,12 +465,16 @@ function AppContent() {
|
|||||||
|
|
||||||
case "usage-dashboard":
|
case "usage-dashboard":
|
||||||
return (
|
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":
|
case "mcp":
|
||||||
return (
|
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":
|
case "project-settings":
|
||||||
|
Reference in New Issue
Block a user