feat(ui): 添加文件 Diff 查看功能
当 AI 执行 write_file 或 edit_file 工具时,在工具结果中显示 View Diff 按钮, 点击后在 IDE 面板中显示文件修改前后的对比视图。 主要改动: - core: edit_file/write_file 工具返回 fileDiff 元数据 - ui: 新增 DiffEditor 组件用于显示文件差异 - ui: ChatMessage 添加 View Diff 按钮 - ui: IDE 组件支持 Diff 视图切换 - ui: useChat hook 处理 fileDiff 回调
This commit is contained in:
@@ -123,9 +123,22 @@ export const editFileTool: ToolWithMetadata = {
|
|||||||
output += `\n\n⚠️ 代码检查发现问题,请修复:${result.diagnostics}`;
|
output += `\n\n⚠️ 代码检查发现问题,请修复:${result.diagnostics}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 构建 diff 元数据
|
||||||
|
const fileName = path.basename(absolutePath);
|
||||||
|
const metadata: Record<string, unknown> = {
|
||||||
|
fileDiff: {
|
||||||
|
path: absolutePath,
|
||||||
|
name: fileName,
|
||||||
|
originalContent: result.originalContent ?? '',
|
||||||
|
newContent: result.newContent ?? '',
|
||||||
|
operation: 'edit',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output,
|
output,
|
||||||
|
metadata,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -92,9 +92,22 @@ export const writeFileTool: ToolWithMetadata = {
|
|||||||
output += `\n\n⚠️ 代码检查发现问题,请修复:${result.diagnostics}`;
|
output += `\n\n⚠️ 代码检查发现问题,请修复:${result.diagnostics}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 构建 diff 元数据
|
||||||
|
const fileName = path.basename(absolutePath);
|
||||||
|
const metadata: Record<string, unknown> = {
|
||||||
|
fileDiff: {
|
||||||
|
path: absolutePath,
|
||||||
|
name: fileName,
|
||||||
|
originalContent: result.originalContent ?? '',
|
||||||
|
newContent: result.newContent ?? content,
|
||||||
|
operation: 'write',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output,
|
output,
|
||||||
|
metadata,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
"@codemirror/lang-json": "^6.0.2",
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
"@codemirror/lang-markdown": "^6.5.0",
|
"@codemirror/lang-markdown": "^6.5.0",
|
||||||
"@codemirror/lang-python": "^6.2.1",
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
|
"@codemirror/merge": "^6.10.2",
|
||||||
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
@@ -39,6 +41,7 @@
|
|||||||
"@uiw/react-codemirror": "^4.25.4",
|
"@uiw/react-codemirror": "^4.25.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
"framer-motion": "^12.23.26",
|
"framer-motion": "^12.23.26",
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ export type {
|
|||||||
SystemCommandListResponse,
|
SystemCommandListResponse,
|
||||||
// Active file types
|
// Active file types
|
||||||
ActiveFileInfo,
|
ActiveFileInfo,
|
||||||
|
// File diff types
|
||||||
|
FileDiffInfo,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
// API Configuration
|
// API Configuration
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ export interface ToolMessagePart {
|
|||||||
result?: unknown;
|
result?: unknown;
|
||||||
error?: string;
|
error?: string;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
|
/** 文件 Diff 信息(write_file/edit_file 工具) */
|
||||||
|
fileDiff?: FileDiffInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1158,3 +1160,21 @@ export interface ActiveFileInfo {
|
|||||||
language: string;
|
language: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ 文件 Diff 相关 ============
|
||||||
|
|
||||||
|
/** 文件 Diff 信息(用于编辑器显示) */
|
||||||
|
export interface FileDiffInfo {
|
||||||
|
/** 文件路径 */
|
||||||
|
path: string;
|
||||||
|
/** 文件名 */
|
||||||
|
name: string;
|
||||||
|
/** 原始内容(修改前) */
|
||||||
|
originalContent: string;
|
||||||
|
/** 新内容(修改后) */
|
||||||
|
newContent: string;
|
||||||
|
/** 操作类型 */
|
||||||
|
operation: 'write' | 'edit';
|
||||||
|
/** 工具调用 ID */
|
||||||
|
toolCallId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
GitCompare,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { useState, forwardRef } from 'react';
|
import { useState, forwardRef } from 'react';
|
||||||
@@ -22,7 +23,7 @@ import { fadeInUp, smoothTransition } from '../utils/animations';
|
|||||||
import { getAgentDisplayName } from '../utils/agent';
|
import { getAgentDisplayName } from '../utils/agent';
|
||||||
import { Markdown } from './Markdown';
|
import { Markdown } from './Markdown';
|
||||||
import { FileMentionText } from './FileMentionTag';
|
import { FileMentionText } from './FileMentionTag';
|
||||||
import type { Message, ToolCallInfo, ToolStatus, ToolMessagePart, QuestionMessagePart } from '../api/types.js';
|
import type { Message, ToolCallInfo, ToolStatus, ToolMessagePart, QuestionMessagePart, FileDiffInfo } from '../api/types.js';
|
||||||
import { AskUserQuestion } from './AskUserQuestion.js';
|
import { AskUserQuestion } from './AskUserQuestion.js';
|
||||||
|
|
||||||
interface ChatMessageProps {
|
interface ChatMessageProps {
|
||||||
@@ -31,10 +32,12 @@ interface ChatMessageProps {
|
|||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
/** 回答问题的回调(用于 ask_user_question 工具) */
|
/** 回答问题的回调(用于 ask_user_question 工具) */
|
||||||
onAnswerQuestion?: (questionPartId: string, answers: string[]) => void;
|
onAnswerQuestion?: (questionPartId: string, answers: string[]) => void;
|
||||||
|
/** 查看文件 Diff 的回调 */
|
||||||
|
onViewDiff?: (diff: FileDiffInfo) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
||||||
({ message, isStreaming = false, onAnswerQuestion }, ref) => {
|
({ message, isStreaming = false, onAnswerQuestion, onViewDiff }, ref) => {
|
||||||
const isUser = message.role === 'user';
|
const isUser = message.role === 'user';
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
@@ -83,7 +86,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'tool':
|
case 'tool':
|
||||||
return <ToolPartItem key={part.id} part={part} />;
|
return <ToolPartItem key={part.id} part={part} onViewDiff={onViewDiff} />;
|
||||||
case 'question': {
|
case 'question': {
|
||||||
// 问题组件:即使在流式输出时也允许用户回答(除非已回答)
|
// 问题组件:即使在流式输出时也允许用户回答(除非已回答)
|
||||||
const questionPart = part as QuestionMessagePart;
|
const questionPart = part as QuestionMessagePart;
|
||||||
@@ -250,15 +253,21 @@ export function TypingIndicator({ agentName }: TypingIndicatorProps) {
|
|||||||
*/
|
*/
|
||||||
interface ToolPartItemProps {
|
interface ToolPartItemProps {
|
||||||
part: ToolMessagePart;
|
part: ToolMessagePart;
|
||||||
|
/** 查看文件 Diff 的回调 */
|
||||||
|
onViewDiff?: (diff: FileDiffInfo) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolPartItem({ part }: ToolPartItemProps) {
|
function ToolPartItem({ part, onViewDiff }: ToolPartItemProps) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const hasDetails =
|
const hasDetails =
|
||||||
Object.keys(part.arguments).length > 0 ||
|
Object.keys(part.arguments).length > 0 ||
|
||||||
part.result !== undefined ||
|
part.result !== undefined ||
|
||||||
part.error !== undefined;
|
part.error !== undefined;
|
||||||
|
|
||||||
|
// 判断是否为文件操作工具且有 diff 数据
|
||||||
|
const isFileOperation = part.toolName === 'write_file' || part.toolName === 'edit_file';
|
||||||
|
const hasDiff = isFileOperation && part.fileDiff && part.status === 'completed';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-line rounded-lg overflow-hidden bg-surface-subtle/30">
|
<div className="border border-line rounded-lg overflow-hidden bg-surface-subtle/30">
|
||||||
{/* 头部:工具名称、状态、时长 */}
|
{/* 头部:工具名称、状态、时长 */}
|
||||||
@@ -278,6 +287,21 @@ function ToolPartItem({ part }: ToolPartItemProps) {
|
|||||||
{part.duration && (
|
{part.duration && (
|
||||||
<span className="text-xs text-fg-subtle">{formatDuration(part.duration)}</span>
|
<span className="text-xs text-fg-subtle">{formatDuration(part.duration)}</span>
|
||||||
)}
|
)}
|
||||||
|
{/* View Diff 按钮 */}
|
||||||
|
{hasDiff && onViewDiff && (
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onViewDiff(part.fileDiff!);
|
||||||
|
}}
|
||||||
|
className="px-2 py-0.5 text-xs bg-primary-500/10 text-primary-400 rounded hover:bg-primary-500/20 transition-colors"
|
||||||
|
>
|
||||||
|
<GitCompare size={12} className="inline mr-1" />
|
||||||
|
View Diff
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
{hasDetails && (
|
{hasDetails && (
|
||||||
<span className="text-fg-subtle">
|
<span className="text-fg-subtle">
|
||||||
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* DiffEditor Component
|
||||||
|
*
|
||||||
|
* 基于 CodeMirror Merge View 的 Diff 编辑器
|
||||||
|
* 用于显示文件修改前后的对比
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useRef, useMemo } from 'react';
|
||||||
|
import { EditorView, basicSetup } from 'codemirror';
|
||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
|
import { MergeView } from '@codemirror/merge';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { python } from '@codemirror/lang-python';
|
||||||
|
import { json } from '@codemirror/lang-json';
|
||||||
|
import { html } from '@codemirror/lang-html';
|
||||||
|
import { css } from '@codemirror/lang-css';
|
||||||
|
import { markdown } from '@codemirror/lang-markdown';
|
||||||
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
|
import { X, FileCode, GitCompare } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { cn } from '../utils/cn.js';
|
||||||
|
import { useTheme } from '../hooks/useTheme.js';
|
||||||
|
import type { FileDiffInfo } from '../api/types.js';
|
||||||
|
|
||||||
|
interface DiffEditorProps {
|
||||||
|
/** Diff 信息 */
|
||||||
|
diff: FileDiffInfo;
|
||||||
|
/** 关闭回调 */
|
||||||
|
onClose?: () => void;
|
||||||
|
/** 容器类名 */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据文件扩展名获取语言
|
||||||
|
function getLanguageExtension(filename: string) {
|
||||||
|
const ext = filename.split('.').pop()?.toLowerCase() || '';
|
||||||
|
switch (ext) {
|
||||||
|
case 'js':
|
||||||
|
case 'jsx':
|
||||||
|
return javascript({ jsx: true });
|
||||||
|
case 'ts':
|
||||||
|
case 'tsx':
|
||||||
|
return javascript({ jsx: true, typescript: true });
|
||||||
|
case 'py':
|
||||||
|
return python();
|
||||||
|
case 'json':
|
||||||
|
return json();
|
||||||
|
case 'html':
|
||||||
|
case 'htm':
|
||||||
|
return html();
|
||||||
|
case 'css':
|
||||||
|
case 'scss':
|
||||||
|
case 'less':
|
||||||
|
return css();
|
||||||
|
case 'md':
|
||||||
|
case 'markdown':
|
||||||
|
return markdown();
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算变更统计
|
||||||
|
function computeChangeStats(original: string, modified: string) {
|
||||||
|
const originalLines = original.split('\n');
|
||||||
|
const modifiedLines = modified.split('\n');
|
||||||
|
|
||||||
|
// 简单统计:计算行数差异
|
||||||
|
const additions = Math.max(0, modifiedLines.length - originalLines.length);
|
||||||
|
const deletions = Math.max(0, originalLines.length - modifiedLines.length);
|
||||||
|
|
||||||
|
// 更精确的统计需要实际的 diff 算法
|
||||||
|
// 这里用简化版本
|
||||||
|
let changed = 0;
|
||||||
|
const minLen = Math.min(originalLines.length, modifiedLines.length);
|
||||||
|
for (let i = 0; i < minLen; i++) {
|
||||||
|
if (originalLines[i] !== modifiedLines[i]) {
|
||||||
|
changed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
additions: additions + changed,
|
||||||
|
deletions: deletions + changed,
|
||||||
|
totalLines: modifiedLines.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DiffEditor({ diff, onClose, className }: DiffEditorProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const mergeViewRef = useRef<MergeView | null>(null);
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
|
// 语言扩展
|
||||||
|
const languageExtension = useMemo(
|
||||||
|
() => getLanguageExtension(diff.name),
|
||||||
|
[diff.name]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 变更统计
|
||||||
|
const stats = useMemo(
|
||||||
|
() => computeChangeStats(diff.originalContent, diff.newContent),
|
||||||
|
[diff.originalContent, diff.newContent]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ESC 键关闭 Diff 视图
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' && onClose) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
// 创建 MergeView
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
// 清理之前的实例
|
||||||
|
if (mergeViewRef.current) {
|
||||||
|
mergeViewRef.current.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建基础扩展
|
||||||
|
const extensions = [
|
||||||
|
basicSetup,
|
||||||
|
EditorView.editable.of(false), // 只读模式
|
||||||
|
EditorState.readOnly.of(true),
|
||||||
|
...(Array.isArray(languageExtension) ? languageExtension : [languageExtension]),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 深色主题
|
||||||
|
if (resolvedTheme === 'dark') {
|
||||||
|
extensions.push(oneDark);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 MergeView
|
||||||
|
const mergeView = new MergeView({
|
||||||
|
a: {
|
||||||
|
doc: diff.originalContent,
|
||||||
|
extensions,
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
doc: diff.newContent,
|
||||||
|
extensions,
|
||||||
|
},
|
||||||
|
parent: containerRef.current,
|
||||||
|
collapseUnchanged: { margin: 3, minSize: 4 },
|
||||||
|
gutter: true,
|
||||||
|
highlightChanges: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
mergeViewRef.current = mergeView;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mergeView.destroy();
|
||||||
|
};
|
||||||
|
}, [diff.originalContent, diff.newContent, languageExtension, resolvedTheme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex flex-col h-full bg-surface-base', className)}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-4 py-2 border-b border-line bg-surface-subtle">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{/* 文件图标和名称 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<GitCompare size={16} className="text-primary-500" />
|
||||||
|
<span className="font-medium text-fg">{diff.name}</span>
|
||||||
|
<span className="text-xs text-fg-muted">({diff.operation === 'write' ? 'Write' : 'Edit'})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 变更统计 */}
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<span className="text-green-500">+{stats.additions}</span>
|
||||||
|
<span className="text-red-500">-{stats.deletions}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* 关闭按钮 */}
|
||||||
|
{onClose && (
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-1.5 rounded hover:bg-surface-muted transition-colors"
|
||||||
|
title="关闭 Diff 视图"
|
||||||
|
>
|
||||||
|
<X size={16} className="text-fg-muted" />
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标签栏 */}
|
||||||
|
<div className="flex border-b border-line bg-surface-subtle">
|
||||||
|
<div className="flex-1 px-4 py-2 text-sm text-fg-muted border-r border-line">
|
||||||
|
<FileCode size={14} className="inline mr-2" />
|
||||||
|
Original
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 px-4 py-2 text-sm text-fg-muted">
|
||||||
|
<FileCode size={14} className="inline mr-2" />
|
||||||
|
Modified
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Diff 视图 */}
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="flex-1 overflow-auto"
|
||||||
|
style={{ minHeight: 0 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 路径提示 */}
|
||||||
|
<div className="px-4 py-1.5 border-t border-line bg-surface-subtle text-xs text-fg-muted truncate">
|
||||||
|
{diff.path}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,13 +4,14 @@
|
|||||||
* 整合文件浏览器和代码编辑器的 IDE 组件
|
* 整合文件浏览器和代码编辑器的 IDE 组件
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
import { useState, useCallback, useEffect, useRef, useImperativeHandle, forwardRef } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { cn } from '../utils/cn.js';
|
import { cn } from '../utils/cn.js';
|
||||||
import { readFile, getWorkingDirectory } from '../api/client.js';
|
import { readFile, getWorkingDirectory } from '../api/client.js';
|
||||||
import { FileExplorer } from './FileExplorer.js';
|
import { FileExplorer } from './FileExplorer.js';
|
||||||
import { CodeEditor, getLanguageFromFilename, type EditorTab } from './CodeEditor.js';
|
import { CodeEditor, getLanguageFromFilename, type EditorTab } from './CodeEditor.js';
|
||||||
import type { ActiveFileInfo } from '../api/types.js';
|
import { DiffEditor } from './DiffEditor.js';
|
||||||
|
import type { ActiveFileInfo, FileDiffInfo } from '../api/types.js';
|
||||||
|
|
||||||
// localStorage 键名
|
// localStorage 键名
|
||||||
const STORAGE_KEY_TABS = 'ai-assistant-editor-tabs';
|
const STORAGE_KEY_TABS = 'ai-assistant-editor-tabs';
|
||||||
@@ -28,12 +29,28 @@ interface IDEProps {
|
|||||||
sidebarWidth?: number;
|
sidebarWidth?: number;
|
||||||
/** 当前活动文件变化回调 */
|
/** 当前活动文件变化回调 */
|
||||||
onActiveFileChange?: (file: ActiveFileInfo | null) => void;
|
onActiveFileChange?: (file: ActiveFileInfo | null) => void;
|
||||||
|
/** 要显示的 Diff 信息(外部传入) */
|
||||||
|
pendingDiff?: FileDiffInfo | null;
|
||||||
|
/** Diff 关闭回调 */
|
||||||
|
onDiffClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IDE({ className, sidebarWidth = 256, onActiveFileChange }: IDEProps) {
|
/** IDE 组件暴露的方法 */
|
||||||
|
export interface IDEHandle {
|
||||||
|
/** 显示文件 diff */
|
||||||
|
showDiff: (diff: FileDiffInfo) => void;
|
||||||
|
/** 关闭 diff 视图 */
|
||||||
|
closeDiff: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IDE = forwardRef<IDEHandle, IDEProps>(function IDE(
|
||||||
|
{ className, sidebarWidth = 256, onActiveFileChange, pendingDiff, onDiffClose },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
const [tabs, setTabs] = useState<EditorTab[]>([]);
|
const [tabs, setTabs] = useState<EditorTab[]>([]);
|
||||||
const [activeTabId, setActiveTabId] = useState<string | null>(null);
|
const [activeTabId, setActiveTabId] = useState<string | null>(null);
|
||||||
const [workingDirectory, setWorkingDirectory] = useState<string>('');
|
const [workingDirectory, setWorkingDirectory] = useState<string>('');
|
||||||
|
const [currentDiff, setCurrentDiff] = useState<FileDiffInfo | null>(null);
|
||||||
const isRestoringTabs = useRef(false);
|
const isRestoringTabs = useRef(false);
|
||||||
const hasRestoredTabs = useRef(false);
|
const hasRestoredTabs = useRef(false);
|
||||||
|
|
||||||
@@ -235,6 +252,47 @@ export function IDE({ className, sidebarWidth = 256, onActiveFileChange }: IDEPr
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 显示 diff 视图
|
||||||
|
const showDiff = useCallback((diff: FileDiffInfo) => {
|
||||||
|
setCurrentDiff(diff);
|
||||||
|
|
||||||
|
// 如果文件已在编辑器中打开,更新其内容
|
||||||
|
setTabs((prev) =>
|
||||||
|
prev.map((tab) => {
|
||||||
|
if (tab.path === diff.path) {
|
||||||
|
return {
|
||||||
|
...tab,
|
||||||
|
content: diff.newContent,
|
||||||
|
originalContent: diff.newContent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return tab;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 关闭 diff 视图
|
||||||
|
const closeDiff = useCallback(() => {
|
||||||
|
setCurrentDiff(null);
|
||||||
|
onDiffClose?.();
|
||||||
|
}, [onDiffClose]);
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
showDiff,
|
||||||
|
closeDiff,
|
||||||
|
}), [showDiff, closeDiff]);
|
||||||
|
|
||||||
|
// 处理外部传入的 pendingDiff
|
||||||
|
useEffect(() => {
|
||||||
|
if (pendingDiff) {
|
||||||
|
showDiff(pendingDiff);
|
||||||
|
}
|
||||||
|
}, [pendingDiff, showDiff]);
|
||||||
|
|
||||||
|
// 判断是否显示 diff 视图
|
||||||
|
const showDiffView = currentDiff !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex h-full', className)}>
|
<div className={cn('flex h-full', className)}>
|
||||||
{/* 文件浏览器 */}
|
{/* 文件浏览器 */}
|
||||||
@@ -245,17 +303,24 @@ export function IDE({ className, sidebarWidth = 256, onActiveFileChange }: IDEPr
|
|||||||
<FileExplorer onFileSelect={handleFileSelect} workingDirectory={workingDirectory} />
|
<FileExplorer onFileSelect={handleFileSelect} workingDirectory={workingDirectory} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 代码编辑器 */}
|
{/* 代码编辑器 / Diff 视图 */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<CodeEditor
|
{showDiffView ? (
|
||||||
tabs={tabs}
|
<DiffEditor
|
||||||
activeTabId={activeTabId}
|
diff={currentDiff}
|
||||||
onTabChange={handleTabChange}
|
onClose={closeDiff}
|
||||||
onTabClose={handleTabClose}
|
/>
|
||||||
onContentChange={handleContentChange}
|
) : (
|
||||||
onSave={handleSave}
|
<CodeEditor
|
||||||
/>
|
tabs={tabs}
|
||||||
|
activeTabId={activeTabId}
|
||||||
|
onTabChange={handleTabChange}
|
||||||
|
onTabClose={handleTabClose}
|
||||||
|
onContentChange={handleContentChange}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import type {
|
|||||||
SubagentToolEndPayload,
|
SubagentToolEndPayload,
|
||||||
SubagentState,
|
SubagentState,
|
||||||
SubagentToolInfo,
|
SubagentToolInfo,
|
||||||
|
FileDiffInfo,
|
||||||
} from '../api/types.js';
|
} from '../api/types.js';
|
||||||
|
|
||||||
interface UseChatOptions {
|
interface UseChatOptions {
|
||||||
@@ -35,6 +36,8 @@ interface UseChatOptions {
|
|||||||
onConfigError?: (error: ConfigErrorPayload) => void;
|
onConfigError?: (error: ConfigErrorPayload) => void;
|
||||||
/** 切换会话回调(如 :new 命令创建新会话) */
|
/** 切换会话回调(如 :new 命令创建新会话) */
|
||||||
onSessionSwitch?: (newSessionId: string) => void;
|
onSessionSwitch?: (newSessionId: string) => void;
|
||||||
|
/** 文件 diff 回调(当 AI 写入/编辑文件时触发) */
|
||||||
|
onFileDiff?: (diff: FileDiffInfo) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatState {
|
interface ChatState {
|
||||||
@@ -54,7 +57,7 @@ interface ChatState {
|
|||||||
currentSubagent: SubagentState | null;
|
currentSubagent: SubagentState | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdated, onConfigError, onSessionSwitch }: UseChatOptions) {
|
export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdated, onConfigError, onSessionSwitch, onFileDiff }: UseChatOptions) {
|
||||||
const [state, setState] = useState<ChatState>({
|
const [state, setState] = useState<ChatState>({
|
||||||
messages: [],
|
messages: [],
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
@@ -80,11 +83,13 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
|||||||
const onSessionUpdatedRef = useRef(onSessionUpdated);
|
const onSessionUpdatedRef = useRef(onSessionUpdated);
|
||||||
const onConfigErrorRef = useRef(onConfigError);
|
const onConfigErrorRef = useRef(onConfigError);
|
||||||
const onSessionSwitchRef = useRef(onSessionSwitch);
|
const onSessionSwitchRef = useRef(onSessionSwitch);
|
||||||
|
const onFileDiffRef = useRef(onFileDiff);
|
||||||
onErrorRef.current = onError;
|
onErrorRef.current = onError;
|
||||||
onSessionNotFoundRef.current = onSessionNotFound;
|
onSessionNotFoundRef.current = onSessionNotFound;
|
||||||
onSessionUpdatedRef.current = onSessionUpdated;
|
onSessionUpdatedRef.current = onSessionUpdated;
|
||||||
onConfigErrorRef.current = onConfigError;
|
onConfigErrorRef.current = onConfigError;
|
||||||
onSessionSwitchRef.current = onSessionSwitch;
|
onSessionSwitchRef.current = onSessionSwitch;
|
||||||
|
onFileDiffRef.current = onFileDiff;
|
||||||
|
|
||||||
// 加载历史消息
|
// 加载历史消息
|
||||||
const loadMessages = useCallback(async () => {
|
const loadMessages = useCallback(async () => {
|
||||||
@@ -244,6 +249,34 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
|||||||
|
|
||||||
case 'tool_end': {
|
case 'tool_end': {
|
||||||
const payload = message.payload as ToolEndPayload;
|
const payload = message.payload as ToolEndPayload;
|
||||||
|
|
||||||
|
// 检查是否为文件写入/编辑工具,提取 diff 信息
|
||||||
|
let fileDiffInfo: FileDiffInfo | undefined;
|
||||||
|
if (payload.status === 'completed' && payload.result) {
|
||||||
|
const result = payload.result as {
|
||||||
|
metadata?: {
|
||||||
|
fileDiff?: {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
originalContent: string;
|
||||||
|
newContent: string;
|
||||||
|
operation: 'write' | 'edit';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.metadata?.fileDiff) {
|
||||||
|
fileDiffInfo = {
|
||||||
|
...result.metadata.fileDiff,
|
||||||
|
toolCallId: payload.id,
|
||||||
|
};
|
||||||
|
// 异步触发回调,避免阻塞状态更新
|
||||||
|
if (onFileDiffRef.current) {
|
||||||
|
setTimeout(() => onFileDiffRef.current?.(fileDiffInfo!), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
if (!prev.streamingMessage) return prev;
|
if (!prev.streamingMessage) return prev;
|
||||||
|
|
||||||
@@ -303,6 +336,7 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
|||||||
result: payload.result,
|
result: payload.result,
|
||||||
error: payload.error,
|
error: payload.error,
|
||||||
duration: payload.duration,
|
duration: payload.duration,
|
||||||
|
fileDiff: fileDiffInfo,
|
||||||
} as ToolMessagePart;
|
} as ToolMessagePart;
|
||||||
}
|
}
|
||||||
return part;
|
return part;
|
||||||
|
|||||||
@@ -201,6 +201,8 @@ export type {
|
|||||||
DiagnosticsResponse,
|
DiagnosticsResponse,
|
||||||
// Active file types
|
// Active file types
|
||||||
ActiveFileInfo,
|
ActiveFileInfo,
|
||||||
|
// File diff types
|
||||||
|
FileDiffInfo,
|
||||||
} from './api/client.js';
|
} from './api/client.js';
|
||||||
|
|
||||||
// Primitives (shadcn/ui style)
|
// Primitives (shadcn/ui style)
|
||||||
@@ -254,7 +256,8 @@ export { DiagnosticsIndicator, DiagnosticsIndicatorCompact } from './components/
|
|||||||
export { SessionPanel } from './components/SessionPanel.js';
|
export { SessionPanel } from './components/SessionPanel.js';
|
||||||
export { FileExplorer } from './components/FileExplorer.js';
|
export { FileExplorer } from './components/FileExplorer.js';
|
||||||
export { CodeEditor, getLanguageFromFilename, type EditorTab } from './components/CodeEditor.js';
|
export { CodeEditor, getLanguageFromFilename, type EditorTab } from './components/CodeEditor.js';
|
||||||
export { IDE } from './components/IDE.js';
|
export { DiffEditor } from './components/DiffEditor.js';
|
||||||
|
export { IDE, type IDEHandle } from './components/IDE.js';
|
||||||
export { StatusBar } from './components/StatusBar.js';
|
export { StatusBar } from './components/StatusBar.js';
|
||||||
export { Resizer } from './components/Resizer.js';
|
export { Resizer } from './components/Resizer.js';
|
||||||
export { ToolbarOverflowMenu, type ToolbarMenuItem } from './components/ToolbarOverflowMenu.js';
|
export { ToolbarOverflowMenu, type ToolbarMenuItem } from './components/ToolbarOverflowMenu.js';
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
createSession,
|
createSession,
|
||||||
type Session,
|
type Session,
|
||||||
type ActiveFileInfo,
|
type ActiveFileInfo,
|
||||||
|
type FileDiffInfo,
|
||||||
} from '@ai-assistant/ui';
|
} from '@ai-assistant/ui';
|
||||||
import { ChatPage } from './pages/Chat';
|
import { ChatPage } from './pages/Chat';
|
||||||
|
|
||||||
@@ -53,6 +54,9 @@ export function App() {
|
|||||||
return saved !== 'false'; // 默认开启
|
return saved !== 'false'; // 默认开启
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Diff 显示状态(当 AI 编辑/写入文件时触发)
|
||||||
|
const [pendingDiff, setPendingDiff] = useState<FileDiffInfo | null>(null);
|
||||||
|
|
||||||
// 持久化自动附加开关状态
|
// 持久化自动附加开关状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('ai-assistant-auto-attach-file', String(autoAttachActiveFile));
|
localStorage.setItem('ai-assistant-auto-attach-file', String(autoAttachActiveFile));
|
||||||
@@ -121,6 +125,16 @@ export function App() {
|
|||||||
setCurrentSessionId(newSessionId);
|
setCurrentSessionId(newSessionId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 文件 diff 回调(当 AI 编辑/写入文件时触发)
|
||||||
|
const handleFileDiff = useCallback((diff: FileDiffInfo) => {
|
||||||
|
setPendingDiff(diff);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Diff 关闭回调
|
||||||
|
const handleDiffClose = useCallback(() => {
|
||||||
|
setPendingDiff(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 处理面板宽度调整
|
// 处理面板宽度调整
|
||||||
const handleResize = useCallback((delta: number) => {
|
const handleResize = useCallback((delta: number) => {
|
||||||
setIdePanelWidth((prev) => {
|
setIdePanelWidth((prev) => {
|
||||||
@@ -159,7 +173,11 @@ export function App() {
|
|||||||
className="hidden md:flex flex-col"
|
className="hidden md:flex flex-col"
|
||||||
style={{ width: `${idePanelWidth}%` }}
|
style={{ width: `${idePanelWidth}%` }}
|
||||||
>
|
>
|
||||||
<IDE onActiveFileChange={setActiveFile} />
|
<IDE
|
||||||
|
onActiveFileChange={setActiveFile}
|
||||||
|
pendingDiff={pendingDiff}
|
||||||
|
onDiffClose={handleDiffClose}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 可拖拽分割线 */}
|
{/* 可拖拽分割线 */}
|
||||||
@@ -191,6 +209,8 @@ export function App() {
|
|||||||
activeFile={activeFile}
|
activeFile={activeFile}
|
||||||
autoAttachActiveFile={autoAttachActiveFile}
|
autoAttachActiveFile={autoAttachActiveFile}
|
||||||
onAutoAttachActiveFileToggle={setAutoAttachActiveFile}
|
onAutoAttachActiveFileToggle={setAutoAttachActiveFile}
|
||||||
|
onFileDiff={handleFileDiff}
|
||||||
|
onViewDiff={handleFileDiff}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex items-center justify-center h-full">
|
<div className="flex-1 flex items-center justify-center h-full">
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
DiagnosticsIndicator,
|
DiagnosticsIndicator,
|
||||||
ToolbarOverflowMenu,
|
ToolbarOverflowMenu,
|
||||||
type ActiveFileInfo,
|
type ActiveFileInfo,
|
||||||
|
type FileDiffInfo,
|
||||||
} from '@ai-assistant/ui';
|
} from '@ai-assistant/ui';
|
||||||
|
|
||||||
interface ChatPageProps {
|
interface ChatPageProps {
|
||||||
@@ -43,6 +44,10 @@ interface ChatPageProps {
|
|||||||
autoAttachActiveFile?: boolean;
|
autoAttachActiveFile?: boolean;
|
||||||
/** 自动附加开关变更回调 */
|
/** 自动附加开关变更回调 */
|
||||||
onAutoAttachActiveFileToggle?: (enabled: boolean) => void;
|
onAutoAttachActiveFileToggle?: (enabled: boolean) => void;
|
||||||
|
/** 文件 diff 回调(当 AI 写入/编辑文件时触发) */
|
||||||
|
onFileDiff?: (diff: FileDiffInfo) => void;
|
||||||
|
/** 查看文件 diff 回调(点击 View Diff 按钮) */
|
||||||
|
onViewDiff?: (diff: FileDiffInfo) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatPage({
|
export function ChatPage({
|
||||||
@@ -63,6 +68,8 @@ export function ChatPage({
|
|||||||
activeFile,
|
activeFile,
|
||||||
autoAttachActiveFile,
|
autoAttachActiveFile,
|
||||||
onAutoAttachActiveFileToggle,
|
onAutoAttachActiveFileToggle,
|
||||||
|
onFileDiff,
|
||||||
|
onViewDiff,
|
||||||
}: ChatPageProps) {
|
}: ChatPageProps) {
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
@@ -100,6 +107,7 @@ export function ChatPage({
|
|||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onFileDiff,
|
||||||
});
|
});
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -208,13 +216,13 @@ export function ChatPage({
|
|||||||
|
|
||||||
<AnimatePresence mode="popLayout">
|
<AnimatePresence mode="popLayout">
|
||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
<ChatMessage key={message.id} message={message} onAnswerQuestion={answerQuestion} />
|
<ChatMessage key={message.id} message={message} onAnswerQuestion={answerQuestion} onViewDiff={onViewDiff ?? onFileDiff} />
|
||||||
))}
|
))}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* 流式消息 - 复用 ChatMessage 组件 */}
|
{/* 流式消息 - 复用 ChatMessage 组件 */}
|
||||||
{streamingMessage && (
|
{streamingMessage && (
|
||||||
<ChatMessage message={streamingMessage} isStreaming onAnswerQuestion={answerQuestion} />
|
<ChatMessage message={streamingMessage} isStreaming onAnswerQuestion={answerQuestion} onViewDiff={onViewDiff ?? onFileDiff} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 子 Agent 进度显示 */}
|
{/* 子 Agent 进度显示 */}
|
||||||
|
|||||||
Generated
+20
@@ -252,6 +252,12 @@ importers:
|
|||||||
'@codemirror/lang-python':
|
'@codemirror/lang-python':
|
||||||
specifier: ^6.2.1
|
specifier: ^6.2.1
|
||||||
version: 6.2.1
|
version: 6.2.1
|
||||||
|
'@codemirror/merge':
|
||||||
|
specifier: ^6.10.2
|
||||||
|
version: 6.11.2
|
||||||
|
'@codemirror/state':
|
||||||
|
specifier: ^6.5.2
|
||||||
|
version: 6.5.2
|
||||||
'@codemirror/theme-one-dark':
|
'@codemirror/theme-one-dark':
|
||||||
specifier: ^6.1.3
|
specifier: ^6.1.3
|
||||||
version: 6.1.3
|
version: 6.1.3
|
||||||
@@ -285,6 +291,9 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
codemirror:
|
||||||
|
specifier: ^6.0.2
|
||||||
|
version: 6.0.2
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^12.23.26
|
specifier: ^12.23.26
|
||||||
version: 12.23.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 12.23.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
@@ -1007,6 +1016,9 @@ packages:
|
|||||||
'@codemirror/lint@6.9.2':
|
'@codemirror/lint@6.9.2':
|
||||||
resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==}
|
resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==}
|
||||||
|
|
||||||
|
'@codemirror/merge@6.11.2':
|
||||||
|
resolution: {integrity: sha512-NO5EJd2rLRbwVWLgMdhIntDIhfDtMOKYEZgqV5WnkNUS2oXOCVWLPjG/kgl/Jth2fGiOuG947bteqxP9nBXmMg==}
|
||||||
|
|
||||||
'@codemirror/search@6.5.11':
|
'@codemirror/search@6.5.11':
|
||||||
resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==}
|
resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==}
|
||||||
|
|
||||||
@@ -5988,6 +6000,14 @@ snapshots:
|
|||||||
'@codemirror/view': 6.39.4
|
'@codemirror/view': 6.39.4
|
||||||
crelt: 1.0.6
|
crelt: 1.0.6
|
||||||
|
|
||||||
|
'@codemirror/merge@6.11.2':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/language': 6.11.3
|
||||||
|
'@codemirror/state': 6.5.2
|
||||||
|
'@codemirror/view': 6.39.4
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
style-mod: 4.1.3
|
||||||
|
|
||||||
'@codemirror/search@6.5.11':
|
'@codemirror/search@6.5.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/state': 6.5.2
|
'@codemirror/state': 6.5.2
|
||||||
|
|||||||
Reference in New Issue
Block a user