修改文件监控逻辑以及UI
This commit is contained in:
@@ -284,6 +284,92 @@ fn create_system_command(
|
|||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Starts watching the Claude projects directory for the specific project
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn watch_claude_project_directory(
|
||||||
|
project_path: String,
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
use crate::file_watcher::FileWatcherState;
|
||||||
|
|
||||||
|
log::info!("Starting to watch Claude project directory for project: {}", project_path);
|
||||||
|
|
||||||
|
let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;
|
||||||
|
let projects_dir = claude_dir.join("projects");
|
||||||
|
|
||||||
|
if !projects_dir.exists() {
|
||||||
|
return Err("Claude projects directory does not exist".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到对应项目的目录
|
||||||
|
if let Ok(entries) = std::fs::read_dir(&projects_dir) {
|
||||||
|
for entry in entries {
|
||||||
|
if let Ok(entry) = entry {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
// 检查是否是当前项目的目录
|
||||||
|
if let Ok(found_project_path) = get_project_path_from_sessions(&path) {
|
||||||
|
if found_project_path == project_path {
|
||||||
|
// 找到了对应的项目目录,开始监控
|
||||||
|
let file_watcher_state = app_handle.state::<FileWatcherState>();
|
||||||
|
let path_str = path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
return file_watcher_state.with_manager(|manager| {
|
||||||
|
manager.watch_path(&path_str, false)
|
||||||
|
}).map_err(|e| format!("Failed to watch Claude project directory: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err("Could not find Claude project directory for the given project path".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops watching the Claude projects directory
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn unwatch_claude_project_directory(
|
||||||
|
project_path: String,
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
use crate::file_watcher::FileWatcherState;
|
||||||
|
|
||||||
|
log::info!("Stopping watch on Claude project directory for project: {}", project_path);
|
||||||
|
|
||||||
|
let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;
|
||||||
|
let projects_dir = claude_dir.join("projects");
|
||||||
|
|
||||||
|
if !projects_dir.exists() {
|
||||||
|
return Ok(()); // 目录不存在,视为成功
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到对应项目的目录
|
||||||
|
if let Ok(entries) = std::fs::read_dir(&projects_dir) {
|
||||||
|
for entry in entries {
|
||||||
|
if let Ok(entry) = entry {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
// 检查是否是当前项目的目录
|
||||||
|
if let Ok(found_project_path) = get_project_path_from_sessions(&path) {
|
||||||
|
if found_project_path == project_path {
|
||||||
|
// 找到了对应的项目目录,停止监控
|
||||||
|
let file_watcher_state = app_handle.state::<FileWatcherState>();
|
||||||
|
let path_str = path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
return file_watcher_state.with_manager(|manager| {
|
||||||
|
manager.unwatch_path(&path_str)
|
||||||
|
}).map_err(|e| format!("Failed to stop watching Claude project directory: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Lists all projects in the ~/.claude/projects directory
|
/// Lists all projects in the ~/.claude/projects directory
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_projects() -> Result<Vec<Project>, String> {
|
pub async fn list_projects() -> Result<Vec<Project>, String> {
|
||||||
|
@@ -30,6 +30,7 @@ use commands::claude::{
|
|||||||
save_claude_md_file, save_claude_settings, save_system_prompt, search_files,
|
save_claude_md_file, save_claude_settings, save_system_prompt, search_files,
|
||||||
track_checkpoint_message, track_session_messages, update_checkpoint_settings,
|
track_checkpoint_message, track_session_messages, update_checkpoint_settings,
|
||||||
get_hooks_config, update_hooks_config, validate_hook_command,
|
get_hooks_config, update_hooks_config, validate_hook_command,
|
||||||
|
watch_claude_project_directory, unwatch_claude_project_directory,
|
||||||
ClaudeProcessState,
|
ClaudeProcessState,
|
||||||
};
|
};
|
||||||
use commands::mcp::{
|
use commands::mcp::{
|
||||||
@@ -237,6 +238,8 @@ fn main() {
|
|||||||
check_claude_version,
|
check_claude_version,
|
||||||
save_system_prompt,
|
save_system_prompt,
|
||||||
save_claude_settings,
|
save_claude_settings,
|
||||||
|
watch_claude_project_directory,
|
||||||
|
unwatch_claude_project_directory,
|
||||||
find_claude_md_files,
|
find_claude_md_files,
|
||||||
read_claude_md_file,
|
read_claude_md_file,
|
||||||
save_claude_md_file,
|
save_claude_md_file,
|
||||||
|
@@ -37,7 +37,7 @@ import { StreamMessage } from "./StreamMessage";
|
|||||||
import { FloatingPromptInput, type FloatingPromptInputRef } from "./FloatingPromptInput";
|
import { FloatingPromptInput, type FloatingPromptInputRef } from "./FloatingPromptInput";
|
||||||
import { TimelineNavigator } from "./TimelineNavigator";
|
import { TimelineNavigator } from "./TimelineNavigator";
|
||||||
import { CheckpointSettings } from "./CheckpointSettings";
|
import { CheckpointSettings } from "./CheckpointSettings";
|
||||||
import { SlashCommandsManager } from "./SlashCommandsManager";
|
import { fileSyncManager } from "@/lib/fileSyncManager";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { SplitPane } from "@/components/ui/split-pane";
|
import { SplitPane } from "@/components/ui/split-pane";
|
||||||
@@ -165,6 +165,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
const [showFileMonitor, setShowFileMonitor] = useState(false);
|
const [showFileMonitor, setShowFileMonitor] = useState(false);
|
||||||
const [isFileWatching, setIsFileWatching] = useState(false);
|
const [isFileWatching, setIsFileWatching] = useState(false);
|
||||||
const [fileMonitorCollapsed, setFileMonitorCollapsed] = useState(false);
|
const [fileMonitorCollapsed, setFileMonitorCollapsed] = useState(false);
|
||||||
|
const [fileMonitorExpanded, setFileMonitorExpanded] = useState(false);
|
||||||
|
|
||||||
// File editor state
|
// File editor state
|
||||||
// 移除重复的状态,使用 layout 中的状态
|
// 移除重复的状态,使用 layout 中的状态
|
||||||
@@ -211,10 +212,19 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
try {
|
try {
|
||||||
console.log('[FileMonitor] Starting file watching for:', projectPath);
|
console.log('[FileMonitor] Starting file watching for:', projectPath);
|
||||||
|
|
||||||
// 启动文件监控
|
// 启动项目目录文件监控
|
||||||
await api.watchDirectory(projectPath, true); // recursive = true
|
await api.watchDirectory(projectPath, true); // recursive = true
|
||||||
|
|
||||||
|
// 启动 Claude 项目目录监控
|
||||||
|
try {
|
||||||
|
await api.watchClaudeProjectDirectory(projectPath);
|
||||||
|
console.log('[FileMonitor] Claude project directory watching started for:', projectPath);
|
||||||
|
} catch (claudeErr) {
|
||||||
|
console.warn('[FileMonitor] Failed to start Claude project directory watching:', claudeErr);
|
||||||
|
// 不影响主要的文件监控功能
|
||||||
|
}
|
||||||
|
|
||||||
setIsFileWatching(true);
|
setIsFileWatching(true);
|
||||||
setShowFileMonitor(true);
|
|
||||||
|
|
||||||
console.log('[FileMonitor] File watching started successfully');
|
console.log('[FileMonitor] File watching started successfully');
|
||||||
|
|
||||||
@@ -231,8 +241,17 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通知文件同步管理器
|
||||||
|
fileSyncManager.notifyFileChange(path, change_type);
|
||||||
|
|
||||||
|
// 判断是否是 Claude 项目文件变化
|
||||||
|
const isClaudeProjectFile = path.includes('/.claude/projects/');
|
||||||
|
const displayPath = isClaudeProjectFile
|
||||||
|
? path.replace(/.*\/\.claude\/projects\/[^/]+\//, '[Claude] ') // 简化 Claude 项目文件路径显示
|
||||||
|
: path.replace(projectPath + '/', ''); // 项目文件相对路径
|
||||||
|
|
||||||
const newChange: FileChange = {
|
const newChange: FileChange = {
|
||||||
path: path.replace(projectPath + '/', ''), // 相对路径
|
path: displayPath,
|
||||||
changeType: change_type,
|
changeType: change_type,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
@@ -242,13 +261,25 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
const updated = [newChange, ...prev].slice(0, 100);
|
const updated = [newChange, ...prev].slice(0, 100);
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 如果是 Claude 项目文件变化且文件被修改,重新加载会话历史
|
||||||
|
if (isClaudeProjectFile && change_type === 'modified' && session) {
|
||||||
|
const fileName = path.split('/').pop() || '';
|
||||||
|
// 检查是否是当前会话的 JSONL 文件
|
||||||
|
if (fileName === `${session.id}.jsonl`) {
|
||||||
|
console.log('[FileMonitor] Claude session file updated, reloading history');
|
||||||
|
// 使用 setTimeout 避免频繁刷新
|
||||||
|
setTimeout(() => {
|
||||||
|
loadSessionHistory();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
fileWatcherUnlistenRef.current = unlisten;
|
fileWatcherUnlistenRef.current = unlisten;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[FileMonitor] Failed to start file watching:', err);
|
console.error('[FileMonitor] Failed to start file watching:', err);
|
||||||
setIsFileWatching(false);
|
setIsFileWatching(false);
|
||||||
setShowFileMonitor(false);
|
|
||||||
}
|
}
|
||||||
}, [projectPath, isFileWatching]);
|
}, [projectPath, isFileWatching]);
|
||||||
|
|
||||||
@@ -265,8 +296,18 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
fileWatcherUnlistenRef.current = null;
|
fileWatcherUnlistenRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止文件监控
|
// 停止项目目录文件监控
|
||||||
await api.unwatchDirectory(projectPath);
|
await api.unwatchDirectory(projectPath);
|
||||||
|
|
||||||
|
// 停止 Claude 项目目录监控
|
||||||
|
try {
|
||||||
|
await api.unwatchClaudeProjectDirectory(projectPath);
|
||||||
|
console.log('[FileMonitor] Claude project directory watching stopped for:', projectPath);
|
||||||
|
} catch (claudeErr) {
|
||||||
|
console.warn('[FileMonitor] Failed to stop Claude project directory watching:', claudeErr);
|
||||||
|
// 不影响主要的停止功能
|
||||||
|
}
|
||||||
|
|
||||||
setIsFileWatching(false);
|
setIsFileWatching(false);
|
||||||
|
|
||||||
// 清空文件变化记录
|
// 清空文件变化记录
|
||||||
@@ -478,6 +519,11 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
|
|
||||||
// After loading history, we're continuing a conversation
|
// After loading history, we're continuing a conversation
|
||||||
setIsFirstPrompt(false);
|
setIsFirstPrompt(false);
|
||||||
|
|
||||||
|
// 加载完成后自动滚动到底部
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, 200);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load session history:", err);
|
console.error("Failed to load session history:", err);
|
||||||
setError("Failed to load session history");
|
setError("Failed to load session history");
|
||||||
@@ -1397,15 +1443,46 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 滚动按钮 */}
|
{/* 滚动按钮和文件监控小点 */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showScrollButtons && (
|
{(showScrollButtons || isFileWatching) && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.8 }}
|
exit={{ opacity: 0, scale: 0.8 }}
|
||||||
className="fixed bottom-20 right-6 z-40 flex flex-col gap-2"
|
className="fixed bottom-20 right-6 z-40 flex flex-col gap-2"
|
||||||
>
|
>
|
||||||
|
{/* 文件监控小绿点 */}
|
||||||
|
{isFileWatching && !fileMonitorExpanded && (
|
||||||
|
<div
|
||||||
|
onClick={() => setFileMonitorExpanded(true)}
|
||||||
|
className="relative cursor-pointer group self-center"
|
||||||
|
>
|
||||||
|
<div className={cn(
|
||||||
|
"w-4 h-4 rounded-full shadow-lg border-2 border-background transition-all duration-200 group-hover:scale-110",
|
||||||
|
isFileWatching ? "bg-green-500" : "bg-gray-400"
|
||||||
|
)}>
|
||||||
|
{/* 脉冲效果 */}
|
||||||
|
{isFileWatching && fileChanges.length > 0 && (
|
||||||
|
<div className="absolute inset-0 rounded-full bg-green-500 animate-ping opacity-30" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 悬浮提示 */}
|
||||||
|
<div className="absolute bottom-full right-0 mb-2 px-2 py-1 bg-background/95 backdrop-blur-sm border rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
文件监控 {fileChanges.length > 0 && `(${fileChanges.length})`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 变化数量小徽章 */}
|
||||||
|
{fileChanges.length > 0 && (
|
||||||
|
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 text-white text-[8px] rounded-full flex items-center justify-center font-bold">
|
||||||
|
{fileChanges.length > 9 ? '9+' : fileChanges.length}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 滚动到顶部按钮 */}
|
||||||
{!isAtTop && (
|
{!isAtTop && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -1425,6 +1502,8 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 滚动到底部按钮 */}
|
||||||
{!isAtBottom && (
|
{!isAtBottom && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -1825,14 +1904,14 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
}
|
}
|
||||||
floatingElements={
|
floatingElements={
|
||||||
<>
|
<>
|
||||||
{/* 文件监控面板 */}
|
{/* 文件监控展开面板 */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showFileMonitor && fileChanges.length > 0 && (
|
{isFileWatching && fileMonitorExpanded && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: 20 }}
|
exit={{ opacity: 0, scale: 0.8 }}
|
||||||
className="absolute bottom-20 right-4 z-30 pointer-events-auto w-80"
|
className="fixed bottom-20 right-4 z-30 pointer-events-auto w-80"
|
||||||
>
|
>
|
||||||
<div className="bg-background/95 backdrop-blur-md border rounded-lg shadow-lg p-3">
|
<div className="bg-background/95 backdrop-blur-md border rounded-lg shadow-lg p-3">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
@@ -1861,6 +1940,14 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setFileMonitorExpanded(false)}
|
||||||
|
className="h-6 w-6"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -41,6 +41,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { fileSyncManager } from "@/lib/fileSyncManager";
|
||||||
|
|
||||||
interface FileEditorEnhancedProps {
|
interface FileEditorEnhancedProps {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
@@ -472,8 +473,39 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
|
|||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [hasChanges, saveFile, isFullscreen]);
|
}, [hasChanges, saveFile, isFullscreen]);
|
||||||
|
|
||||||
// 使用真正的文件系统监听
|
// 使用文件同步管理器监听文件变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!filePath) return;
|
||||||
|
|
||||||
|
const listenerId = `file-editor-${filePath}`;
|
||||||
|
|
||||||
|
// 注册文件变化监听器
|
||||||
|
fileSyncManager.registerChangeListener(
|
||||||
|
listenerId,
|
||||||
|
filePath,
|
||||||
|
(changedPath, changeType) => {
|
||||||
|
// 检查是否是当前文件的变化
|
||||||
|
if (changedPath === filePath && (changeType === 'modified' || changeType === 'created')) {
|
||||||
|
// 检查时间间隔,避免自己保存触发的事件
|
||||||
|
const timeSinceLastSave = Date.now() - lastCheckTime;
|
||||||
|
|
||||||
|
if (timeSinceLastSave > 1000) { // 超过1秒,可能是外部修改
|
||||||
|
console.log('[FileEditor] File changed externally via FileSyncManager:', changedPath, changeType);
|
||||||
|
setFileChanged(true);
|
||||||
|
|
||||||
|
// 如果没有未保存的更改,自动重新加载
|
||||||
|
if (!hasChanges) {
|
||||||
|
console.log('[FileEditor] Auto-reloading file content');
|
||||||
|
loadFile();
|
||||||
|
} else {
|
||||||
|
// 显示提示
|
||||||
|
setError("文件已被外部程序修改,点击重新加载按钮查看最新内容");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const setupFileWatcher = async () => {
|
const setupFileWatcher = async () => {
|
||||||
if (!filePath) return;
|
if (!filePath) return;
|
||||||
|
|
||||||
@@ -485,7 +517,7 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
|
|||||||
recursive: false
|
recursive: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听文件变化事件
|
// 监听文件变化事件(作为备用)
|
||||||
unlistenRef.current = await listen('file-system-change', (event: any) => {
|
unlistenRef.current = await listen('file-system-change', (event: any) => {
|
||||||
const { path, change_type } = event.payload;
|
const { path, change_type } = event.payload;
|
||||||
|
|
||||||
@@ -495,7 +527,7 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
|
|||||||
const timeSinceLastSave = Date.now() - lastCheckTime;
|
const timeSinceLastSave = Date.now() - lastCheckTime;
|
||||||
|
|
||||||
if (timeSinceLastSave > 1000) { // 超过1秒,可能是外部修改
|
if (timeSinceLastSave > 1000) { // 超过1秒,可能是外部修改
|
||||||
console.log('File changed externally:', path, change_type);
|
console.log('[FileEditor] File changed externally (fallback):', path, change_type);
|
||||||
setFileChanged(true);
|
setFileChanged(true);
|
||||||
|
|
||||||
// 如果没有未保存的更改,自动重新加载
|
// 如果没有未保存的更改,自动重新加载
|
||||||
@@ -553,6 +585,9 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
|
|||||||
|
|
||||||
// 清理函数
|
// 清理函数
|
||||||
return () => {
|
return () => {
|
||||||
|
// 注销文件同步管理器监听器
|
||||||
|
fileSyncManager.unregisterListener(listenerId, filePath);
|
||||||
|
|
||||||
// 停止监听
|
// 停止监听
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
const dirPath = filePath.substring(0, filePath.lastIndexOf('/'));
|
const dirPath = filePath.substring(0, filePath.lastIndexOf('/'));
|
||||||
|
@@ -2482,5 +2482,35 @@ export const api = {
|
|||||||
console.error("Failed to unwatch directory:", error);
|
console.error("Failed to unwatch directory:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============= Claude Project Directory Watching =============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts watching Claude project directory for the given project path
|
||||||
|
* @param projectPath - The project path to find the corresponding Claude directory
|
||||||
|
* @returns Promise resolving when watching starts
|
||||||
|
*/
|
||||||
|
async watchClaudeProjectDirectory(projectPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
return await invoke<void>("watch_claude_project_directory", { projectPath });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to watch Claude project directory:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops watching Claude project directory for the given project path
|
||||||
|
* @param projectPath - The project path to find the corresponding Claude directory
|
||||||
|
* @returns Promise resolving when watching stops
|
||||||
|
*/
|
||||||
|
async unwatchClaudeProjectDirectory(projectPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
return await invoke<void>("unwatch_claude_project_directory", { projectPath });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to unwatch Claude project directory:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
255
src/lib/fileSyncManager.ts
Normal file
255
src/lib/fileSyncManager.ts
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
/**
|
||||||
|
* 文件同步管理器 - 管理文件变化通知和内容同步
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface FileChangeListener {
|
||||||
|
id: string;
|
||||||
|
filePath: string;
|
||||||
|
callback: (filePath: string, changeType: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileContentListener {
|
||||||
|
id: string;
|
||||||
|
filePath: string;
|
||||||
|
callback: (filePath: string, newContent: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileSyncManager {
|
||||||
|
private changeListeners: Map<string, FileChangeListener[]> = new Map();
|
||||||
|
private contentListeners: Map<string, FileContentListener[]> = new Map();
|
||||||
|
private static instance: FileSyncManager | null = null;
|
||||||
|
|
||||||
|
public static getInstance(): FileSyncManager {
|
||||||
|
if (!FileSyncManager.instance) {
|
||||||
|
FileSyncManager.instance = new FileSyncManager();
|
||||||
|
}
|
||||||
|
return FileSyncManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册文件变化监听器
|
||||||
|
* @param id 监听器唯一标识
|
||||||
|
* @param filePath 文件路径(可以是相对路径或绝对路径)
|
||||||
|
* @param callback 变化回调函数
|
||||||
|
*/
|
||||||
|
public registerChangeListener(
|
||||||
|
id: string,
|
||||||
|
filePath: string,
|
||||||
|
callback: (filePath: string, changeType: string) => void
|
||||||
|
): void {
|
||||||
|
const normalizedPath = this.normalizePath(filePath);
|
||||||
|
|
||||||
|
if (!this.changeListeners.has(normalizedPath)) {
|
||||||
|
this.changeListeners.set(normalizedPath, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners = this.changeListeners.get(normalizedPath)!;
|
||||||
|
|
||||||
|
// 移除已存在的相同ID监听器
|
||||||
|
const existingIndex = listeners.findIndex(l => l.id === id);
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
listeners.splice(existingIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新监听器
|
||||||
|
listeners.push({ id, filePath: normalizedPath, callback });
|
||||||
|
|
||||||
|
console.log(`[FileSyncManager] Registered change listener ${id} for ${normalizedPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册文件内容监听器
|
||||||
|
* @param id 监听器唯一标识
|
||||||
|
* @param filePath 文件路径
|
||||||
|
* @param callback 内容更新回调函数
|
||||||
|
*/
|
||||||
|
public registerContentListener(
|
||||||
|
id: string,
|
||||||
|
filePath: string,
|
||||||
|
callback: (filePath: string, newContent: string) => void
|
||||||
|
): void {
|
||||||
|
const normalizedPath = this.normalizePath(filePath);
|
||||||
|
|
||||||
|
if (!this.contentListeners.has(normalizedPath)) {
|
||||||
|
this.contentListeners.set(normalizedPath, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners = this.contentListeners.get(normalizedPath)!;
|
||||||
|
|
||||||
|
// 移除已存在的相同ID监听器
|
||||||
|
const existingIndex = listeners.findIndex(l => l.id === id);
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
listeners.splice(existingIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新监听器
|
||||||
|
listeners.push({ id, filePath: normalizedPath, callback });
|
||||||
|
|
||||||
|
console.log(`[FileSyncManager] Registered content listener ${id} for ${normalizedPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注销监听器
|
||||||
|
* @param id 监听器ID
|
||||||
|
* @param filePath 文件路径(可选)
|
||||||
|
*/
|
||||||
|
public unregisterListener(id: string, filePath?: string): void {
|
||||||
|
if (filePath) {
|
||||||
|
const normalizedPath = this.normalizePath(filePath);
|
||||||
|
|
||||||
|
// 从变化监听器中移除
|
||||||
|
const changeListeners = this.changeListeners.get(normalizedPath);
|
||||||
|
if (changeListeners) {
|
||||||
|
const index = changeListeners.findIndex(l => l.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
changeListeners.splice(index, 1);
|
||||||
|
if (changeListeners.length === 0) {
|
||||||
|
this.changeListeners.delete(normalizedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从内容监听器中移除
|
||||||
|
const contentListeners = this.contentListeners.get(normalizedPath);
|
||||||
|
if (contentListeners) {
|
||||||
|
const index = contentListeners.findIndex(l => l.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
contentListeners.splice(index, 1);
|
||||||
|
if (contentListeners.length === 0) {
|
||||||
|
this.contentListeners.delete(normalizedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 移除所有该ID的监听器
|
||||||
|
for (const [path, listeners] of this.changeListeners.entries()) {
|
||||||
|
const index = listeners.findIndex(l => l.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
listeners.splice(index, 1);
|
||||||
|
if (listeners.length === 0) {
|
||||||
|
this.changeListeners.delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [path, listeners] of this.contentListeners.entries()) {
|
||||||
|
const index = listeners.findIndex(l => l.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
listeners.splice(index, 1);
|
||||||
|
if (listeners.length === 0) {
|
||||||
|
this.contentListeners.delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[FileSyncManager] Unregistered listener ${id}${filePath ? ` for ${filePath}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知文件变化
|
||||||
|
* @param filePath 变化的文件路径
|
||||||
|
* @param changeType 变化类型
|
||||||
|
*/
|
||||||
|
public notifyFileChange(filePath: string, changeType: string): void {
|
||||||
|
const normalizedPath = this.normalizePath(filePath);
|
||||||
|
const listeners = this.changeListeners.get(normalizedPath);
|
||||||
|
|
||||||
|
if (listeners && listeners.length > 0) {
|
||||||
|
console.log(`[FileSyncManager] Notifying ${listeners.length} change listeners for ${normalizedPath}`);
|
||||||
|
|
||||||
|
listeners.forEach(listener => {
|
||||||
|
try {
|
||||||
|
listener.callback(normalizedPath, changeType);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[FileSyncManager] Error in change listener ${listener.id}:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知文件内容更新
|
||||||
|
* @param filePath 文件路径
|
||||||
|
* @param newContent 新内容
|
||||||
|
*/
|
||||||
|
public notifyContentUpdate(filePath: string, newContent: string): void {
|
||||||
|
const normalizedPath = this.normalizePath(filePath);
|
||||||
|
const listeners = this.contentListeners.get(normalizedPath);
|
||||||
|
|
||||||
|
if (listeners && listeners.length > 0) {
|
||||||
|
console.log(`[FileSyncManager] Notifying ${listeners.length} content listeners for ${normalizedPath}`);
|
||||||
|
|
||||||
|
listeners.forEach(listener => {
|
||||||
|
try {
|
||||||
|
listener.callback(normalizedPath, newContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[FileSyncManager] Error in content listener ${listener.id}:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化文件路径
|
||||||
|
* @param filePath 原始文件路径
|
||||||
|
* @returns 规范化后的路径
|
||||||
|
*/
|
||||||
|
private normalizePath(filePath: string): string {
|
||||||
|
return filePath
|
||||||
|
.replace(/\\/g, '/') // 统一使用正斜杠
|
||||||
|
.replace(/\/+/g, '/') // 移除重复斜杠
|
||||||
|
.replace(/\/$/, ''); // 移除结尾斜杠
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查路径是否匹配(支持相对路径匹配)
|
||||||
|
* @param watchPath 监听的路径
|
||||||
|
* @param changePath 变化的路径
|
||||||
|
* @returns 是否匹配
|
||||||
|
*/
|
||||||
|
public pathMatches(watchPath: string, changePath: string): boolean {
|
||||||
|
const normalizedWatchPath = this.normalizePath(watchPath);
|
||||||
|
const normalizedChangePath = this.normalizePath(changePath);
|
||||||
|
|
||||||
|
// 完全匹配
|
||||||
|
if (normalizedWatchPath === normalizedChangePath) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为相对路径匹配
|
||||||
|
const watchPathParts = normalizedWatchPath.split('/');
|
||||||
|
const changePathParts = normalizedChangePath.split('/');
|
||||||
|
|
||||||
|
// 如果监听路径更短,检查是否为后缀匹配
|
||||||
|
if (watchPathParts.length <= changePathParts.length) {
|
||||||
|
const offset = changePathParts.length - watchPathParts.length;
|
||||||
|
for (let i = 0; i < watchPathParts.length; i++) {
|
||||||
|
if (watchPathParts[i] !== changePathParts[i + offset]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前注册的监听器统计
|
||||||
|
*/
|
||||||
|
public getStats(): { changeListeners: number; contentListeners: number; totalFiles: number } {
|
||||||
|
const changeListenerCount = Array.from(this.changeListeners.values()).reduce((sum, arr) => sum + arr.length, 0);
|
||||||
|
const contentListenerCount = Array.from(this.contentListeners.values()).reduce((sum, arr) => sum + arr.length, 0);
|
||||||
|
const totalFiles = new Set([...this.changeListeners.keys(), ...this.contentListeners.keys()]).size;
|
||||||
|
|
||||||
|
return {
|
||||||
|
changeListeners: changeListenerCount,
|
||||||
|
contentListeners: contentListenerCount,
|
||||||
|
totalFiles
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fileSyncManager = FileSyncManager.getInstance();
|
||||||
|
export default fileSyncManager;
|
Reference in New Issue
Block a user