"use client"; import React from "react"; import { AnimatePresence, motion } from "framer-motion"; import { Box, Button, Chip, CircularProgress, Collapse, IconButton, Stack, Typography, alpha, useTheme, } from "@mui/material"; import type { Theme } from "@mui/material/styles"; import TerminalRounded from "@mui/icons-material/TerminalRounded"; import FolderOpenRounded from "@mui/icons-material/FolderOpenRounded"; import CheckCircleRounded from "@mui/icons-material/CheckCircleRounded"; import BlockRounded from "@mui/icons-material/BlockRounded"; import PushPinRounded from "@mui/icons-material/PushPinRounded"; import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRounded"; import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded"; import VerifiedUserRounded from "@mui/icons-material/VerifiedUserRounded"; import type { PermissionReply } from "@/lib/chatStream"; import type { Message } from "./GlobalChatbox.types"; const formatMetadataValue = (value: unknown) => { if (typeof value === "string") { return value; } try { return JSON.stringify(value); } catch { return "[unserializable]"; } }; const truncateText = (value: string, maxLength: number) => value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value; const formatMetadata = (metadata: Record) => { const entries = Object.entries(metadata) .filter(([key]) => !["command", "path", "file", "directory"].includes(key)) .slice(0, 3); if (!entries.length) { return ""; } return entries .map(([key, value]) => `${key}: ${truncateText(formatMetadataValue(value), 64)}`) .join(";"); }; const getPermissionTitle = (permission: NonNullable[number]) => { if (permission.permission === "external_directory") return "访问工作区外目录"; if (permission.permission === "bash") return "执行终端命令"; if (permission.permission === "edit") return "修改文件内容"; return permission.permission || "工具权限请求"; }; const getPermissionPrimaryValue = ( permission: NonNullable[number], ) => { const command = permission.metadata.command; if (typeof command === "string" && command.trim()) { return command.trim(); } for (const key of ["path", "file", "directory"]) { const value = permission.metadata[key]; if (typeof value === "string" && value.trim()) { return value.trim(); } } return permission.patterns[0] ?? permission.permission; }; const PermissionIcon = ({ permission, }: { permission: NonNullable[number]; }) => { if (permission.permission === "bash") { return ; } if (permission.permission === "external_directory") { return ; } return ; }; const getPermissionStatusLabel = (status: NonNullable[number]["status"]) => { if (status === "approved_always") return "已始终允许"; if (status === "approved_once") return "已允许一次"; if (status === "rejected") return "已拒绝"; if (status === "aborted") return "已中断"; if (status === "error") return "提交失败"; if (status === "submitting") return "提交中"; return "等待确认"; }; const pendingPermissionColor = "#f9a825"; const approvedOncePermissionColor = "#00838f"; const getPermissionStatusColor = ( status: NonNullable[number]["status"], theme: Theme, ) => { if (status === "approved_once") return approvedOncePermissionColor; if (status === "approved_always") return theme.palette.success.main; if (status === "rejected" || status === "error") return theme.palette.error.main; if (status === "aborted") return theme.palette.text.secondary; return pendingPermissionColor; }; const getPermissionStatusTextColor = ( status: NonNullable[number]["status"], theme: Theme, ) => { if (status === "approved_once") return "#006c78"; if (status === "approved_always") return theme.palette.success.dark; if (status === "rejected" || status === "error") return theme.palette.error.main; if (status === "aborted") return theme.palette.text.secondary; return "#8a5a00"; }; const PermissionRequestCard = ({ permission, isRunning, onReply, }: { permission: NonNullable[number]; isRunning: boolean; onReply: (requestId: string, reply: PermissionReply) => void; }) => { const theme = useTheme(); const isPending = isRunning && (permission.status === "pending" || permission.status === "error"); const isSubmitting = isRunning && permission.status === "submitting"; const primaryValue = getPermissionPrimaryValue(permission); const metadataText = formatMetadata(permission.metadata); const accentColor = getPermissionStatusColor(permission.status, theme); const statusTextColor = getPermissionStatusTextColor(permission.status, theme); const statusLabel = getPermissionStatusLabel(permission.status); return ( {getPermissionTitle(permission)} 请求目标 {primaryValue} {metadataText ? ( {metadataText} ) : null} {permission.error ? ( {permission.error} ) : null} {isPending || isSubmitting ? ( ) : null} ); }; export const PermissionRequestGroup = ({ permissions, isRunning, onReply, }: { permissions: NonNullable; isRunning: boolean; onReply: (requestId: string, reply: PermissionReply) => void; }) => { const theme = useTheme(); const onceCount = permissions.filter((permission) => permission.status === "approved_once").length; const alwaysCount = permissions.filter((permission) => permission.status === "approved_always").length; const rejectedCount = permissions.filter((permission) => permission.status === "rejected").length; const abortedCount = permissions.filter((permission) => permission.status === "aborted").length; const pendingCount = permissions.filter( (permission) => permission.status === "pending" || permission.status === "submitting" || permission.status === "error", ).length; const hasPendingPermissions = pendingCount > 0; const [expanded, setExpanded] = React.useState(false); const latestPermissions = permissions.slice(-3); const pendingPermissions = permissions.filter( (permission) => permission.status === "pending" || permission.status === "submitting" || permission.status === "error", ); const summaryItems = [ { label: "共", value: permissions.length, color: theme.palette.text.secondary }, { label: "允许一次", value: onceCount, color: getPermissionStatusColor("approved_once", theme), textColor: getPermissionStatusTextColor("approved_once", theme) }, { label: "始终允许", value: alwaysCount, color: getPermissionStatusColor("approved_always", theme), textColor: getPermissionStatusTextColor("approved_always", theme) }, { label: "拒绝", value: rejectedCount, color: getPermissionStatusColor("rejected", theme), textColor: getPermissionStatusTextColor("rejected", theme) }, { label: "中断", value: abortedCount, color: getPermissionStatusColor("aborted", theme), textColor: getPermissionStatusTextColor("aborted", theme) }, ]; const chipColor = pendingCount > 0 ? getPermissionStatusColor("pending", theme) : abortedCount > 0 ? getPermissionStatusColor("aborted", theme) : rejectedCount > 0 ? getPermissionStatusColor("rejected", theme) : getPermissionStatusColor("approved_always", theme); const chipTextColor = pendingCount > 0 ? getPermissionStatusTextColor("pending", theme) : abortedCount > 0 ? getPermissionStatusTextColor("aborted", theme) : rejectedCount > 0 ? getPermissionStatusTextColor("rejected", theme) : getPermissionStatusTextColor("approved_always", theme); return ( setExpanded((value) => !value)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); setExpanded((value) => !value); } }} sx={{ px: 1.5, py: 1.15, cursor: "pointer", transition: "background-color 0.2s ease", "&:hover": { bgcolor: alpha("#000", 0.025) }, }} > 权限请求 {summaryItems.map((item) => ( {item.label} {item.value} 项 ))} {isRunning && pendingCount > 0 ? ( ) : null} {expanded ? ( ) : ( )} {!expanded && isRunning && !hasPendingPermissions && latestPermissions.length > 0 ? ( {latestPermissions.map((permission, index) => { const primaryValue = getPermissionPrimaryValue(permission); const isLast = index === latestPermissions.length - 1; const itemColor = getPermissionStatusColor(permission.status, theme); const itemTextColor = getPermissionStatusTextColor(permission.status, theme); return ( {getPermissionTitle(permission)} {truncateText(primaryValue, 72)} ); })} ) : null} {!expanded && isRunning && hasPendingPermissions ? ( {pendingPermissions.map((permission) => ( ))} ) : null} {permissions.map((permission) => ( ))} ); };