diff --git a/src/components/ClaudeCodeSession.tsx b/src/components/ClaudeCodeSession.tsx index d7b1bab..0a30521 100644 --- a/src/components/ClaudeCodeSession.tsx +++ b/src/components/ClaudeCodeSession.tsx @@ -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 = ({ // Add collapsed state for queued prompts const [queuedPromptsCollapsed, setQueuedPromptsCollapsed] = useState(false); + // 文件监控相关状态 + const [fileChanges, setFileChanges] = useState([]); + const [showFileMonitor, setShowFileMonitor] = useState(false); + const [isFileWatching, setIsFileWatching] = useState(false); + const [fileMonitorCollapsed, setFileMonitorCollapsed] = useState(false); + // File editor state // 移除重复的状态,使用 layout 中的状态 // const [editingFile, setEditingFile] = useState(null); // 移除,使用 layout.editingFile @@ -158,6 +178,7 @@ export const ClaudeCodeSession: React.FC = ({ const isMountedRef = useRef(true); const isListeningRef = useRef(false); const sessionStartTime = useRef(Date.now()); + const fileWatcherUnlistenRef = useRef(null); // Session metrics state for enhanced analytics const sessionMetrics = useRef({ @@ -183,6 +204,97 @@ export const ClaudeCodeSession: React.FC = ({ // 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('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 = ({ 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 = ({ )} + {/* File Monitor Toggle */} + {projectPath && ( + + + + + + +

{isFileWatching ? '停止文件监控' : '启动文件监控'}

+
+
+
+ )} + {projectPath && onProjectSettings && ( @@ -1679,6 +1825,96 @@ export const ClaudeCodeSession: React.FC = ({ } floatingElements={ <> + {/* 文件监控面板 */} + + {showFileMonitor && fileChanges.length > 0 && ( + +
+
+
+ + 文件变化监控 +
+
+
+ + +
+
+ + {!fileMonitorCollapsed && ( +
+ {fileChanges.map((change, index) => { + const getChangeIcon = () => { + switch (change.changeType) { + case 'created': + return ; + case 'modified': + return ; + case 'deleted': + return ; + case 'renamed': + return ; + default: + return ; + } + }; + + return ( + + {getChangeIcon()} +
+
+ {change.path} +
+
+ {change.changeType} • {new Date(change.timestamp).toLocaleTimeString()} +
+
+
+ ); + })} + + {fileChanges.length === 0 && isFileWatching && ( +
+ 监控中,等待文件变化... +
+ )} +
+ )} +
+ + )} + + {/* 排队提示显示 */} {queuedPrompts.length > 0 && ( diff --git a/src/components/FileExplorerPanelEnhanced.tsx b/src/components/FileExplorerPanelEnhanced.tsx index 3343726..0d6630f 100644 --- a/src/components/FileExplorerPanelEnhanced.tsx +++ b/src/components/FileExplorerPanelEnhanced.tsx @@ -306,6 +306,35 @@ export const FileExplorerPanelEnhanced: React.FC 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 if (!isVisible) return null; return ( -
+
{/* Header */}
diff --git a/src/components/FloatingPromptInput.tsx b/src/components/FloatingPromptInput.tsx index 9a4b286..b565203 100644 --- a/src/components/FloatingPromptInput.tsx +++ b/src/components/FloatingPromptInput.tsx @@ -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" diff --git a/src/lib/api.ts b/src/lib/api.ts index ee0c6f2..ef3d22f 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -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 { + try { + return await invoke("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 { + try { + return await invoke("unwatch_directory", { path: directoryPath }); + } catch (error) { + console.error("Failed to unwatch directory:", error); + throw error; + } } };