diff --git a/packages/server/src/permission/handler.ts b/packages/server/src/permission/handler.ts index 8f1c7ea..9a7e8f0 100644 --- a/packages/server/src/permission/handler.ts +++ b/packages/server/src/permission/handler.ts @@ -9,6 +9,8 @@ import type { PermissionRequestPayload, PermissionDisplayContext, ServerMessage, + DiffInfo, + DiffHunkInfo, } from '../types.js'; import { inferPermissionType } from '@ai-assistant/core'; import type { PermissionDecision, PermissionContext, PermissionType } from '@ai-assistant/core'; @@ -79,6 +81,73 @@ function isAutoApproved(sessionId: string, ctx: PermissionContext): boolean { return false; } +/** + * 生成简单的行级别 diff 信息 + * 用于在权限确认对话框中显示文件变更预览 + */ +function generateDiffInfo(oldContent: string | undefined, newContent: string | undefined): DiffInfo | undefined { + // 如果没有内容,无法生成 diff + if (newContent === undefined) { + return undefined; + } + + const isNew = !oldContent; + const oldLines = oldContent ? oldContent.split('\n') : []; + const newLines = newContent.split('\n'); + + // 简单的行级别 diff + const lines: DiffHunkInfo['lines'] = []; + let additions = 0; + let deletions = 0; + + if (isNew) { + // 新文件:所有行都是添加 + newLines.forEach((line, index) => { + lines.push({ + type: 'add', + lineNumber: index + 1, + content: line, + }); + additions++; + }); + } else { + // 编辑:显示删除和添加 + // 对于 edit_file(search-replace),oldContent 是被替换的部分,newContent 是替换后的部分 + oldLines.forEach((line, index) => { + lines.push({ + type: 'remove', + lineNumber: index + 1, + content: line, + }); + deletions++; + }); + newLines.forEach((line, index) => { + lines.push({ + type: 'add', + lineNumber: index + 1, + content: line, + }); + additions++; + }); + } + + // 构建单个 hunk + const hunk: DiffHunkInfo = { + oldStart: 1, + oldCount: oldLines.length, + newStart: 1, + newCount: newLines.length, + lines, + }; + + return { + isNew, + additions, + deletions, + hunks: [hunk], + }; +} + /** * 构建权限请求显示上下文 * 将 Core 的完整 PermissionContext 转换为用于前端显示的精简格式 @@ -135,11 +204,18 @@ export function createServerPermissionCallback(sessionId: string) { const permissionType = inferPermissionType(permCtx); const context = buildDisplayContext(permCtx); + // 为文件操作生成 diff + let diff: DiffInfo | undefined; + if (permissionType === 'file' && (permCtx.fileOperation === 'write' || permCtx.fileOperation === 'edit')) { + diff = generateDiffInfo(permCtx.oldContent, permCtx.newContent); + } + // 构建请求 payload const payload: PermissionRequestPayload = { requestId, permissionType, context, + diff, }; // 发送权限请求到客户端 diff --git a/packages/ui/src/components/PermissionDialog.tsx b/packages/ui/src/components/PermissionDialog.tsx index 04aab61..f03f137 100644 --- a/packages/ui/src/components/PermissionDialog.tsx +++ b/packages/ui/src/components/PermissionDialog.tsx @@ -15,6 +15,9 @@ import { X, Check, AlertTriangle, + ChevronDown, + ChevronUp, + Diff, } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { cn } from '../utils/cn'; @@ -166,6 +169,7 @@ export function PermissionDialog({ responsive = false, }: PermissionDialogProps) { const [remember, setRemember] = useState(false); + const [showDiff, setShowDiff] = useState(false); const { requestId, permissionType, context, diff } = request; const handleAllow = () => { @@ -205,22 +209,39 @@ export function PermissionDialog({ case 'file': return (
-
- Operation: - - {context.operation?.toUpperCase()} - +
+
+ Operation: + + {context.operation?.toUpperCase()} + +
+ {diff && ( + + )}
Path:
{context.path} - {diff && } + {diff && showDiff && }
);