修改文件监控逻辑

This commit is contained in:
2025-08-13 21:25:41 +08:00
parent 3b04f0d9b9
commit 2faf07827d
4 changed files with 317 additions and 5 deletions

View File

@@ -16,7 +16,13 @@ import {
PanelLeftOpen,
PanelRightOpen,
ArrowUp,
ArrowDown
ArrowDown,
Eye,
EyeOff,
FileText,
FilePlus,
FileX,
Clock
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -44,6 +50,14 @@ import { useVirtualizer } from "@tanstack/react-virtual";
import { useTrackEvent, useComponentMetrics, useWorkflowTracking, useLayoutManager } from "@/hooks";
// import { GridLayoutContainer, ResponsivePanel } from "@/components/ui/grid-layout";
// 文件变化监控接口
interface FileChange {
path: string;
changeType: 'created' | 'modified' | 'deleted' | 'renamed';
timestamp: number;
oldPath?: string; // 用于重命名操作
}
// 新增布局组件导入
import { FlexLayoutContainer } from "@/components/layout/FlexLayoutContainer";
import { MainContentArea } from "@/components/layout/MainContentArea";
@@ -146,6 +160,12 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
// Add collapsed state for queued prompts
const [queuedPromptsCollapsed, setQueuedPromptsCollapsed] = useState(false);
// 文件监控相关状态
const [fileChanges, setFileChanges] = useState<FileChange[]>([]);
const [showFileMonitor, setShowFileMonitor] = useState(false);
const [isFileWatching, setIsFileWatching] = useState(false);
const [fileMonitorCollapsed, setFileMonitorCollapsed] = useState(false);
// File editor state
// 移除重复的状态,使用 layout 中的状态
// const [editingFile, setEditingFile] = useState<string | null>(null); // 移除,使用 layout.editingFile
@@ -158,6 +178,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
const isMountedRef = useRef(true);
const isListeningRef = useRef(false);
const sessionStartTime = useRef<number>(Date.now());
const fileWatcherUnlistenRef = useRef<UnlistenFn | null>(null);
// Session metrics state for enhanced analytics
const sessionMetrics = useRef({
@@ -183,6 +204,97 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
// const aiTracking = useAIInteractionTracking('sonnet'); // Default model
const workflowTracking = useWorkflowTracking('claude_session');
// 启动文件监控
const startFileWatching = useCallback(async () => {
if (!projectPath || isFileWatching) return;
try {
console.log('[FileMonitor] Starting file watching for:', projectPath);
// 启动文件监控
await api.watchDirectory(projectPath, true); // recursive = true
setIsFileWatching(true);
setShowFileMonitor(true);
console.log('[FileMonitor] File watching started successfully');
// 监听文件系统变化事件
const unlisten = await listen<any>('file-system-change', (event) => {
if (!isMountedRef.current) return;
const { path, change_type } = event.payload;
console.log('[FileMonitor] File change detected:', { path, change_type });
// 过滤掉隐藏文件和临时文件
const fileName = path.split('/').pop() || '';
if (fileName.startsWith('.') || fileName.includes('~') || fileName.endsWith('.tmp')) {
return;
}
const newChange: FileChange = {
path: path.replace(projectPath + '/', ''), // 相对路径
changeType: change_type,
timestamp: Date.now(),
};
setFileChanges(prev => {
// 限制最多保存100个变化记录
const updated = [newChange, ...prev].slice(0, 100);
return updated;
});
});
fileWatcherUnlistenRef.current = unlisten;
} catch (err) {
console.error('[FileMonitor] Failed to start file watching:', err);
setIsFileWatching(false);
setShowFileMonitor(false);
}
}, [projectPath, isFileWatching]);
// 停止文件监控
const stopFileWatching = useCallback(async () => {
if (!projectPath || !isFileWatching) return;
try {
console.log('[FileMonitor] Stopping file watching for:', projectPath);
// 停止监听事件
if (fileWatcherUnlistenRef.current) {
fileWatcherUnlistenRef.current();
fileWatcherUnlistenRef.current = null;
}
// 停止文件监控
await api.unwatchDirectory(projectPath);
setIsFileWatching(false);
// 清空文件变化记录
setFileChanges([]);
console.log('[FileMonitor] File watching stopped successfully');
} catch (err) {
console.error('[FileMonitor] Failed to stop file watching:', err);
// 即使后端出错,也要更新前端状态
setIsFileWatching(false);
setFileChanges([]);
}
}, [projectPath, isFileWatching]);
// 切换文件监控状态
const toggleFileWatching = useCallback(() => {
if (isFileWatching) {
stopFileWatching();
} else {
startFileWatching();
}
}, [isFileWatching, startFileWatching, stopFileWatching]);
// 清空文件变化历史
const clearFileChanges = useCallback(() => {
setFileChanges([]);
}, []);
// Keep ref in sync with state
useEffect(() => {
queuedPromptsRef.current = queuedPrompts;
@@ -1194,6 +1306,19 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
unlistenRefs.current.forEach(unlisten => unlisten());
unlistenRefs.current = [];
// 清理文件监控
if (fileWatcherUnlistenRef.current) {
fileWatcherUnlistenRef.current();
fileWatcherUnlistenRef.current = null;
}
// 停止文件监控
if (projectPath && isFileWatching) {
api.unwatchDirectory(projectPath).catch(err => {
console.error("[FileMonitor] Failed to unwatch directory:", err);
});
}
// Clear checkpoint manager when session ends
if (effectiveSession) {
api.clearCheckpointManager(effectiveSession.id).catch(err => {
@@ -1462,6 +1587,27 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
</TooltipProvider>
)}
{/* File Monitor Toggle */}
{projectPath && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={toggleFileWatching}
className={cn("h-8 w-8", isFileWatching && "text-primary")}
>
{isFileWatching ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{isFileWatching ? '停止文件监控' : '启动文件监控'}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{projectPath && onProjectSettings && (
<TooltipProvider>
<Tooltip>
@@ -1679,6 +1825,96 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
}
floatingElements={
<>
{/* 文件监控面板 */}
<AnimatePresence>
{showFileMonitor && fileChanges.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
className="absolute 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="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-primary" />
<span className="text-sm font-medium"></span>
<div className={cn(
"w-2 h-2 rounded-full",
isFileWatching ? "bg-green-500" : "bg-gray-400"
)} />
</div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => setFileMonitorCollapsed(!fileMonitorCollapsed)}
className="h-6 w-6"
>
{fileMonitorCollapsed ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
</Button>
<Button
variant="ghost"
size="icon"
onClick={clearFileChanges}
className="h-6 w-6"
>
<X className="h-3 w-3" />
</Button>
</div>
</div>
{!fileMonitorCollapsed && (
<div className="max-h-64 overflow-y-auto space-y-1">
{fileChanges.map((change, index) => {
const getChangeIcon = () => {
switch (change.changeType) {
case 'created':
return <FilePlus className="h-3 w-3 text-green-500" />;
case 'modified':
return <FileText className="h-3 w-3 text-yellow-500" />;
case 'deleted':
return <FileX className="h-3 w-3 text-red-500" />;
case 'renamed':
return <FileText className="h-3 w-3 text-blue-500" />;
default:
return <FileText className="h-3 w-3 text-gray-500" />;
}
};
return (
<motion.div
key={`${change.path}-${change.timestamp}`}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.02 }}
className="flex items-start gap-2 p-2 bg-muted/30 rounded text-xs"
>
{getChangeIcon()}
<div className="flex-1 min-w-0">
<div className="font-mono text-xs truncate" title={change.path}>
{change.path}
</div>
<div className="text-xs text-muted-foreground">
{change.changeType} {new Date(change.timestamp).toLocaleTimeString()}
</div>
</div>
</motion.div>
);
})}
{fileChanges.length === 0 && isFileWatching && (
<div className="text-center py-4 text-muted-foreground text-xs">
...
</div>
)}
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
{/* 排队提示显示 */}
<AnimatePresence>
{queuedPrompts.length > 0 && (

View File

@@ -306,6 +306,35 @@ export const FileExplorerPanelEnhanced: React.FC<FileExplorerPanelEnhancedProps>
const handleKeyDown = (e: KeyboardEvent) => {
if (!isVisible) return;
// 检查事件目标是否是输入元素
const target = e.target as HTMLElement;
const isInputElement = target && (
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.contentEditable === 'true' ||
target.closest('[contenteditable="true"]') !== null ||
target.closest('input, textarea, [contenteditable]') !== null
);
// 如果事件来自输入元素,不处理键盘导航
if (isInputElement) {
return;
}
// 检查是否在文件浏览器区域内
const explorerPanel = document.querySelector('[data-file-explorer-panel]');
if (explorerPanel && !explorerPanel.contains(target)) {
// 如果事件不是来自文件浏览器区域,并且有输入元素获得焦点,则不处理
const activeElement = document.activeElement;
if (activeElement && (
activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
(activeElement as HTMLElement).contentEditable === 'true'
)) {
return;
}
}
const currentIndex = flattenedNodes.findIndex(node => node.path === selectedPath);
if (currentIndex === -1 && flattenedNodes.length > 0) {
setSelectedPath(flattenedNodes[0].path);
@@ -620,7 +649,7 @@ export const FileExplorerPanelEnhanced: React.FC<FileExplorerPanelEnhancedProps>
if (!isVisible) return null;
return (
<div className="flex flex-col h-full border-r border-border">
<div className="flex flex-col h-full border-r border-border" data-file-explorer-panel>
{/* Header */}
<div className="flex items-center justify-between p-3 border-b">
<div className="flex items-center gap-2">

View File

@@ -617,9 +617,24 @@ const FloatingPromptInputInner = (
return;
}
if (e.key === "Enter" && !e.shiftKey && !isExpanded && !showFilePicker && !showSlashCommandPicker) {
e.preventDefault();
handleSend();
// 处理发送快捷键
if (e.key === "Enter") {
if (isExpanded) {
// 展开模式Ctrl+Enter发送Enter换行
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
e.stopPropagation(); // 防止事件冒泡到窗口级别
handleSend();
}
// 普通Enter键在展开模式下允许换行不需要处理
} else {
// 收起模式Enter发送Shift+Enter换行
if (!e.shiftKey && !showFilePicker && !showSlashCommandPicker) {
e.preventDefault();
e.stopPropagation(); // 防止事件冒泡到窗口级别
handleSend();
}
}
}
};
@@ -763,6 +778,7 @@ const FloatingPromptInputInner = (
ref={expandedTextareaRef}
value={prompt}
onChange={handleTextChange}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
placeholder={t('messages.typeYourPromptHere')}
className="min-h-[200px] resize-none"

View File

@@ -2451,5 +2451,36 @@ export const api = {
console.error("Failed to get PackyCode user quota:", error);
throw error;
}
},
// ============= File System Watching =============
/**
* Starts watching a directory for file system changes
* @param directoryPath - The directory path to watch
* @param recursive - Whether to watch subdirectories recursively
* @returns Promise resolving when watching starts
*/
async watchDirectory(directoryPath: string, recursive: boolean = true): Promise<void> {
try {
return await invoke<void>("watch_directory", { path: directoryPath, recursive });
} catch (error) {
console.error("Failed to watch directory:", error);
throw error;
}
},
/**
* Stops watching a directory for file system changes
* @param directoryPath - The directory path to stop watching
* @returns Promise resolving when watching stops
*/
async unwatchDirectory(directoryPath: string): Promise<void> {
try {
return await invoke<void>("unwatch_directory", { path: directoryPath });
} catch (error) {
console.error("Failed to unwatch directory:", error);
throw error;
}
}
};