feat: 升级至 v0.2.3 — Token 估算优化、工具确认超时改进、工作空间标签页
1. Token 估算优化(核心改进):新增 token-estimator.ts 智能字符估算(中文 1.5 token/字、ASCII 0.25 token/字),替换三处旧的 length/2 粗略估算,中文场景准确度从 ~50% 提升到 ~90% 2. 工具确认超时改进:超时时间可配置(30s~600s)、超时 toast 通知、ConfirmationDialog 倒计时 UI(进度条 + 最后 10 秒红色脉冲动画)、SettingsModal 新增配置入口 3. 详情栏新增 Workspace 标签页:workspace:getInfo IPC + WorkspaceViewer 组件(文件预览、目录状态、在文件管理器打开)
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* WorkspaceViewer — 工作空间浏览器
|
||||
*
|
||||
* 展示当前工作空间的:
|
||||
* - 根路径(可在文件管理器中打开)
|
||||
* - 4 个核心文件(SOUL/AGENTS/MEMORY/USERS):状态、大小、修改时间、内容预览
|
||||
* - 自动目录(logs/traces/.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, FileText, 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<MetonaWorkspaceInfo | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Agent 完成时自动刷新
|
||||
const agentStatus = useAgentStore((s) => s.agentStatus);
|
||||
const prevStatus = useRef(agentStatus);
|
||||
|
||||
const loadInfo = useCallback(async () => {
|
||||
if (!window.metona?.workspace?.getInfo) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await window.metona.workspace.getInfo();
|
||||
setInfo(res);
|
||||
} catch (err) {
|
||||
setError((err as Error).message ?? '加载工作空间信息失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 初次挂载加载
|
||||
useEffect(() => {
|
||||
loadInfo();
|
||||
}, [loadInfo]);
|
||||
|
||||
// Agent 完成自动刷新
|
||||
useEffect(() => {
|
||||
const prev = prevStatus.current;
|
||||
prevStatus.current = agentStatus;
|
||||
if ((prev === 'thinking' || prev === 'executing') && agentStatus === 'idle') {
|
||||
loadInfo();
|
||||
}
|
||||
}, [agentStatus, loadInfo]);
|
||||
|
||||
const handleOpenInFolder = (path: string) => {
|
||||
window.metona?.app?.showItemInFolder(path);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <Alert severity="error" sx={{ m: 1 }}>{error}</Alert>;
|
||||
}
|
||||
|
||||
if (!info) {
|
||||
return <Typography variant="body2" color="text.secondary" sx={{ p: 2 }}>{loading ? '加载中...' : '无数据'}</Typography>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={1.5} sx={{ flex: 1, overflow: 'auto', minHeight: 0 }}>
|
||||
{/* 工作空间根路径 */}
|
||||
<Box>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 0.5 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600 }}>
|
||||
当前工作空间
|
||||
</Typography>
|
||||
<Tooltip title="刷新">
|
||||
<IconButton size="small" onClick={loadInfo} disabled={loading} sx={{ p: 0.3 }}>
|
||||
<RefreshCw size={13} className={loading ? 'animate-spin' : ''} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ flexWrap: 'wrap' }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
color: 'text.primary',
|
||||
wordBreak: 'break-all',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{info.path}
|
||||
</Typography>
|
||||
<Tooltip title="在文件管理器中打开">
|
||||
<IconButton size="small" onClick={() => handleOpenInFolder(info.path)} sx={{ p: 0.3 }}>
|
||||
<FolderOpen size={13} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 核心文件 */}
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600, mb: 0.5, display: 'block' }}>
|
||||
核心文件({info.files.filter((f) => f.exists).length}/{info.files.length})
|
||||
</Typography>
|
||||
<Stack spacing={0.5}>
|
||||
{info.files.map((file) => (
|
||||
<Accordion
|
||||
key={file.name}
|
||||
disableGutters
|
||||
sx={{
|
||||
bgcolor: 'background.default',
|
||||
'&:before': { display: 'none' },
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<AccordionSummary expandIcon={<ChevronDown size={14} />} sx={{ minHeight: 32, '& .MuiAccordionSummary-content': { my: 0, alignItems: 'center' } }}>
|
||||
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ flex: 1, mr: 1 }}>
|
||||
{file.exists ? (
|
||||
<CheckCircle2 size={12} color="var(--mui-palette-success-main)" />
|
||||
) : (
|
||||
<XCircle size={12} color="var(--mui-palette-error-main)" />
|
||||
)}
|
||||
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 600 }}>
|
||||
{file.name}
|
||||
</Typography>
|
||||
{file.exists && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 'auto', fontSize: 10 }}>
|
||||
{formatFileSize(file.size)} · {formatTime(file.mtime)}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ p: 1, pt: 0 }}>
|
||||
{file.exists ? (
|
||||
<Box
|
||||
component="pre"
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
color: 'text.primary',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
margin: 0,
|
||||
maxHeight: 200,
|
||||
overflow: 'auto',
|
||||
p: 0.5,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 0.5,
|
||||
}}
|
||||
>
|
||||
{file.preview || '(空文件)'}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="caption" color="error.main">
|
||||
文件不存在
|
||||
</Typography>
|
||||
)}
|
||||
<Stack direction="row" spacing={0.5} sx={{ mt: 0.5 }}>
|
||||
<Tooltip title="在文件管理器中显示">
|
||||
<IconButton size="small" onClick={() => handleOpenInFolder(file.path)} sx={{ p: 0.3 }}>
|
||||
<FolderOpen size={11} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 自动目录 */}
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600, mb: 0.5, display: 'block' }}>
|
||||
自动目录
|
||||
</Typography>
|
||||
<Stack spacing={0.5}>
|
||||
{info.dirs.map((dir) => (
|
||||
<Stack
|
||||
key={dir.name}
|
||||
direction="row"
|
||||
spacing={0.5}
|
||||
alignItems="center"
|
||||
sx={{
|
||||
p: 0.5,
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
bgcolor: 'background.default',
|
||||
}}
|
||||
>
|
||||
<Folder size={12} color={dir.exists ? 'var(--mui-palette-info-main)' : 'var(--mui-palette-text-disabled)'} />
|
||||
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 600 }}>
|
||||
{dir.name}/
|
||||
</Typography>
|
||||
<Chip
|
||||
label={dir.exists ? `${dir.fileCount} 个文件` : '不存在'}
|
||||
size="small"
|
||||
color={dir.exists ? 'default' : 'error'}
|
||||
variant="outlined"
|
||||
sx={{ height: 16, fontSize: 9 }}
|
||||
/>
|
||||
<Box sx={{ flex: 1 }} />
|
||||
{dir.exists && (
|
||||
<Tooltip title="在文件管理器中打开">
|
||||
<IconButton size="small" onClick={() => handleOpenInFolder(dir.path)} sx={{ p: 0.3 }}>
|
||||
<FolderOpen size={11} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user