From 1801298dce49ef58120b90e7c041ae94947b0038 Mon Sep 17 00:00:00 2001 From: kurihada Date: Thu, 18 Dec 2025 11:16:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=9D=83=E9=99=90=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E5=AF=B9=E8=AF=9D=E6=A1=86=E6=94=AF=E6=8C=81=20Diff?= =?UTF-8?q?=20=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 Diff 按钮,点击可展开/折叠文件变更预览 - Server 端生成 diff 信息并传递到前端 - 默认折叠,用户可按需查看 --- packages/server/src/permission/handler.ts | 76 +++++++++++++++++++ .../ui/src/components/PermissionDialog.tsx | 43 ++++++++--- 2 files changed, 108 insertions(+), 11 deletions(-) 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 && }
);