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:
thzxx
2026-07-12 14:20:05 +08:00
parent 0363176215
commit 1eabed9b70
13 changed files with 563 additions and 35 deletions
+72 -2
View File
@@ -13,8 +13,9 @@ import {
Dialog, DialogTitle, DialogContent, DialogActions,
Button, Typography, Box, Chip, Alert, FormControlLabel, Checkbox,
Accordion, AccordionSummary, AccordionDetails,
LinearProgress,
} from '@mui/material';
import { ShieldAlert, ChevronDown } from 'lucide-react';
import { ShieldAlert, ChevronDown, Timer } from 'lucide-react';
interface ConfirmationRequest {
toolCallId: string;
@@ -22,6 +23,8 @@ interface ConfirmationRequest {
args: Record<string, unknown>;
riskLevel: string;
reason: string;
/** 过期时间戳(ms),由后端 ConfirmationHook 注入,用于倒计时 */
expiresAt?: number;
}
const RISK_COLORS: Record<string, 'default' | 'success' | 'warning' | 'error' | 'info'> = {
@@ -36,17 +39,45 @@ export function ConfirmationDialog(): React.JSX.Element | null {
const [request, setRequest] = useState<ConfirmationRequest | null>(null);
const [remember, setRemember] = useState(false);
const [autoExecute, setAutoExecute] = useState(false);
const [remainingMs, setRemainingMs] = useState<number>(0);
// 记录首次接收时的剩余时间,作为进度条总量(固定不变)
const [initialMs, setInitialMs] = useState<number>(0);
useEffect(() => {
// 监听来自主进程的确认请求(通过 preload 暴露的 metona.tool API
const cleanup = window.metona?.tool?.onConfirmationRequest((data: unknown) => {
setRequest(data as ConfirmationRequest);
const req = data as ConfirmationRequest;
setRequest(req);
setRemember(false);
setAutoExecute(false);
// 初始化倒计时
if (req.expiresAt) {
const remain = Math.max(0, req.expiresAt - Date.now());
setRemainingMs(remain);
setInitialMs(remain);
} else {
setRemainingMs(0);
setInitialMs(0);
}
});
return cleanup;
}, []);
// 倒计时:每 200ms 更新剩余时间
useEffect(() => {
if (!request?.expiresAt) return;
const interval = setInterval(() => {
const remaining = request.expiresAt! - Date.now();
if (remaining <= 0) {
setRemainingMs(0);
clearInterval(interval);
} else {
setRemainingMs(remaining);
}
}, 200);
return () => clearInterval(interval);
}, [request]);
const handleRespond = useCallback((approved: boolean) => {
if (!request) return;
// 通过 preload 暴露的 metona.tool API 发送响应
@@ -63,6 +94,14 @@ export function ConfirmationDialog(): React.JSX.Element | null {
if (!request) return null;
const riskColor = RISK_COLORS[request.riskLevel] ?? 'default';
const remainingSec = Math.ceil(remainingMs / 1000);
const isUrgent = remainingSec <= 10 && remainingSec > 0;
const isExpired = remainingMs <= 0 && request.expiresAt != null;
// 总时长使用首次接收时的剩余时间(固定值),保证进度条单调递减
const totalMs = initialMs || remainingMs;
const progressPercent = totalMs > 0
? Math.max(0, Math.min(100, (remainingMs / totalMs) * 100))
: 100;
// 格式化参数显示
const formatArg = (key: string, value: unknown): string => {
@@ -101,6 +140,37 @@ export function ConfirmationDialog(): React.JSX.Element | null {
{request.reason}
</Alert>
{request.expiresAt && !isExpired && (
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
<Timer
size={16}
color={isUrgent ? 'var(--mui-palette-error-main)' : 'var(--mui-palette-text-secondary)'}
style={isUrgent ? { animation: 'metona-pulse 1s ease-in-out infinite' } : undefined}
/>
<Typography
variant="caption"
sx={{
color: isUrgent ? 'error.main' : 'text.secondary',
fontWeight: isUrgent ? 700 : 500,
minWidth: 48,
animation: isUrgent ? 'metona-pulse 1s ease-in-out infinite' : 'none',
'@keyframes metona-pulse': {
'0%, 100%': { opacity: 1 },
'50%': { opacity: 0.4 },
},
}}
>
{remainingSec}s
</Typography>
<LinearProgress
variant="determinate"
value={progressPercent}
color={isUrgent ? 'error' : 'primary'}
sx={{ flex: 1, height: 6, borderRadius: 3 }}
/>
</Box>
)}
<Box sx={{ mb: 2, display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Chip
label={`工具: ${request.toolName}`}