/** * WorkspaceViewer — 工作空间浏览器 * * 展示当前工作空间的: * - 根路径(可在文件管理器中打开) * - 2 个核心文件(SOUL/MEMORY):状态、大小、修改时间、内容预览 * - 自动目录(logs/.metona):存在性和文件数 * * Agent 完成任务后自动刷新(thinking/executing → idle)。 */ import { useState, useEffect, useRef, useCallback } from 'react'; import { Box, Typography, Stack, Accordion, AccordionSummary, AccordionDetails, IconButton, Chip, Alert, Tooltip, Divider, } from '@mui/material'; import { FolderOpen, RefreshCw, ChevronDown, Folder, CheckCircle2, XCircle } from 'lucide-react'; import { useAgentStore } from '@renderer/stores/agent-store'; import { formatTime, formatFileSize } from '@renderer/lib/formatters'; // ===== 主组件 ===== export function WorkspaceViewer(): React.JSX.Element { const [info, setInfo] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); // Agent 完成时自动刷新 const agentStatus = useAgentStore((s) => s.agentStatus); const prevStatus = useRef(agentStatus); // M-52 修复: 竞态保护 ref,防止多次 loadInfo 调用乱序完成导致旧数据覆盖 const loadReqIdRef = useRef(0); const loadInfo = useCallback(async () => { if (!window.metona?.workspace?.getInfo) return; const reqId = ++loadReqIdRef.current; setLoading(true); setError(null); try { const res = await window.metona.workspace.getInfo(); // 竞态保护:若已被新请求取代或组件已卸载,放弃本次结果 if (loadReqIdRef.current !== reqId) return; setInfo(res); } catch (err) { if (loadReqIdRef.current !== reqId) return; setError((err as Error).message ?? '加载工作空间信息失败'); } finally { if (loadReqIdRef.current === reqId) setLoading(false); } }, []); // 初次挂载加载 useEffect(() => { loadInfo(); // cleanup: 使当前请求失效(防止卸载后 setState) return () => { loadReqIdRef.current++; }; }, [loadInfo]); // Agent 完成自动刷新 useEffect(() => { const prev = prevStatus.current; prevStatus.current = agentStatus; if ((prev === 'thinking' || prev === 'executing') && agentStatus === 'idle') { loadInfo(); } }, [agentStatus, loadInfo]); const handleOpenInFolder = async (path: string) => { try { await window.metona?.app?.showItemInFolder(path); } catch (err) { console.error('[WorkspaceViewer]', err); import('@metona-team/metona-toast').then((mod) => mod.default.error('打开文件夹失败')).catch(() => {}); } }; if (error) { return {error}; } if (!info) { return {loading ? '加载中...' : '无数据'}; } return ( {/* 工作空间根路径 */} 当前工作空间 {info.path} handleOpenInFolder(info.path)} sx={{ p: 0.3 }}> {/* 核心文件 */} 核心文件({info.files.filter((f) => f.exists).length}/{info.files.length}) {info.files.map((file) => ( } sx={{ minHeight: 32, '& .MuiAccordionSummary-content': { my: 0, alignItems: 'center' } }}> {file.exists ? ( ) : ( )} {file.name} {file.exists && ( {formatFileSize(file.size)} · {formatTime(file.mtime)} )} {file.exists ? ( {file.preview || '(空文件)'} ) : ( 文件不存在 )} handleOpenInFolder(file.path)} sx={{ p: 0.3 }}> ))} {/* 自动目录 */} 自动目录 {info.dirs.map((dir) => ( {dir.name}/ {dir.exists && ( handleOpenInFolder(dir.path)} sx={{ p: 0.3 }}> )} ))} ); }