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:
@@ -153,6 +153,8 @@ export type {
|
||||
SystemCommandListResponse,
|
||||
// Active file types
|
||||
ActiveFileInfo,
|
||||
// File diff types
|
||||
FileDiffInfo,
|
||||
} from './types.js';
|
||||
|
||||
// API Configuration
|
||||
|
||||
@@ -57,6 +57,8 @@ export interface ToolMessagePart {
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
duration?: number;
|
||||
/** 文件 Diff 信息(write_file/edit_file 工具) */
|
||||
fileDiff?: FileDiffInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1158,3 +1160,21 @@ export interface ActiveFileInfo {
|
||||
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,
|
||||
CheckCircle2,
|
||||
Loader2,
|
||||
GitCompare,
|
||||
} from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useState, forwardRef } from 'react';
|
||||
@@ -22,7 +23,7 @@ import { fadeInUp, smoothTransition } from '../utils/animations';
|
||||
import { getAgentDisplayName } from '../utils/agent';
|
||||
import { Markdown } from './Markdown';
|
||||
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';
|
||||
|
||||
interface ChatMessageProps {
|
||||
@@ -31,10 +32,12 @@ interface ChatMessageProps {
|
||||
isStreaming?: boolean;
|
||||
/** 回答问题的回调(用于 ask_user_question 工具) */
|
||||
onAnswerQuestion?: (questionPartId: string, answers: string[]) => void;
|
||||
/** 查看文件 Diff 的回调 */
|
||||
onViewDiff?: (diff: FileDiffInfo) => void;
|
||||
}
|
||||
|
||||
export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
||||
({ message, isStreaming = false, onAnswerQuestion }, ref) => {
|
||||
({ message, isStreaming = false, onAnswerQuestion, onViewDiff }, ref) => {
|
||||
const isUser = message.role === 'user';
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
@@ -83,7 +86,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
||||
</div>
|
||||
);
|
||||
case 'tool':
|
||||
return <ToolPartItem key={part.id} part={part} />;
|
||||
return <ToolPartItem key={part.id} part={part} onViewDiff={onViewDiff} />;
|
||||
case 'question': {
|
||||
// 问题组件:即使在流式输出时也允许用户回答(除非已回答)
|
||||
const questionPart = part as QuestionMessagePart;
|
||||
@@ -250,15 +253,21 @@ export function TypingIndicator({ agentName }: TypingIndicatorProps) {
|
||||
*/
|
||||
interface ToolPartItemProps {
|
||||
part: ToolMessagePart;
|
||||
/** 查看文件 Diff 的回调 */
|
||||
onViewDiff?: (diff: FileDiffInfo) => void;
|
||||
}
|
||||
|
||||
function ToolPartItem({ part }: ToolPartItemProps) {
|
||||
function ToolPartItem({ part, onViewDiff }: ToolPartItemProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const hasDetails =
|
||||
Object.keys(part.arguments).length > 0 ||
|
||||
part.result !== undefined ||
|
||||
part.error !== undefined;
|
||||
|
||||
// 判断是否为文件操作工具且有 diff 数据
|
||||
const isFileOperation = part.toolName === 'write_file' || part.toolName === 'edit_file';
|
||||
const hasDiff = isFileOperation && part.fileDiff && part.status === 'completed';
|
||||
|
||||
return (
|
||||
<div className="border border-line rounded-lg overflow-hidden bg-surface-subtle/30">
|
||||
{/* 头部:工具名称、状态、时长 */}
|
||||
@@ -278,6 +287,21 @@ function ToolPartItem({ part }: ToolPartItemProps) {
|
||||
{part.duration && (
|
||||
<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 && (
|
||||
<span className="text-fg-subtle">
|
||||
{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 组件
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { useState, useCallback, useEffect, useRef, useImperativeHandle, forwardRef } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '../utils/cn.js';
|
||||
import { readFile, getWorkingDirectory } from '../api/client.js';
|
||||
import { FileExplorer } from './FileExplorer.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 键名
|
||||
const STORAGE_KEY_TABS = 'ai-assistant-editor-tabs';
|
||||
@@ -28,12 +29,28 @@ interface IDEProps {
|
||||
sidebarWidth?: number;
|
||||
/** 当前活动文件变化回调 */
|
||||
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 [activeTabId, setActiveTabId] = useState<string | null>(null);
|
||||
const [workingDirectory, setWorkingDirectory] = useState<string>('');
|
||||
const [currentDiff, setCurrentDiff] = useState<FileDiffInfo | null>(null);
|
||||
const isRestoringTabs = 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 (
|
||||
<div className={cn('flex h-full', className)}>
|
||||
{/* 文件浏览器 */}
|
||||
@@ -245,17 +303,24 @@ export function IDE({ className, sidebarWidth = 256, onActiveFileChange }: IDEPr
|
||||
<FileExplorer onFileSelect={handleFileSelect} workingDirectory={workingDirectory} />
|
||||
</div>
|
||||
|
||||
{/* 代码编辑器 */}
|
||||
{/* 代码编辑器 / Diff 视图 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<CodeEditor
|
||||
tabs={tabs}
|
||||
activeTabId={activeTabId}
|
||||
onTabChange={handleTabChange}
|
||||
onTabClose={handleTabClose}
|
||||
onContentChange={handleContentChange}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
{showDiffView ? (
|
||||
<DiffEditor
|
||||
diff={currentDiff}
|
||||
onClose={closeDiff}
|
||||
/>
|
||||
) : (
|
||||
<CodeEditor
|
||||
tabs={tabs}
|
||||
activeTabId={activeTabId}
|
||||
onTabChange={handleTabChange}
|
||||
onTabClose={handleTabClose}
|
||||
onContentChange={handleContentChange}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
SubagentToolEndPayload,
|
||||
SubagentState,
|
||||
SubagentToolInfo,
|
||||
FileDiffInfo,
|
||||
} from '../api/types.js';
|
||||
|
||||
interface UseChatOptions {
|
||||
@@ -35,6 +36,8 @@ interface UseChatOptions {
|
||||
onConfigError?: (error: ConfigErrorPayload) => void;
|
||||
/** 切换会话回调(如 :new 命令创建新会话) */
|
||||
onSessionSwitch?: (newSessionId: string) => void;
|
||||
/** 文件 diff 回调(当 AI 写入/编辑文件时触发) */
|
||||
onFileDiff?: (diff: FileDiffInfo) => void;
|
||||
}
|
||||
|
||||
interface ChatState {
|
||||
@@ -54,7 +57,7 @@ interface ChatState {
|
||||
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>({
|
||||
messages: [],
|
||||
isConnected: false,
|
||||
@@ -80,11 +83,13 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
||||
const onSessionUpdatedRef = useRef(onSessionUpdated);
|
||||
const onConfigErrorRef = useRef(onConfigError);
|
||||
const onSessionSwitchRef = useRef(onSessionSwitch);
|
||||
const onFileDiffRef = useRef(onFileDiff);
|
||||
onErrorRef.current = onError;
|
||||
onSessionNotFoundRef.current = onSessionNotFound;
|
||||
onSessionUpdatedRef.current = onSessionUpdated;
|
||||
onConfigErrorRef.current = onConfigError;
|
||||
onSessionSwitchRef.current = onSessionSwitch;
|
||||
onFileDiffRef.current = onFileDiff;
|
||||
|
||||
// 加载历史消息
|
||||
const loadMessages = useCallback(async () => {
|
||||
@@ -244,6 +249,34 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
||||
|
||||
case 'tool_end': {
|
||||
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) => {
|
||||
if (!prev.streamingMessage) return prev;
|
||||
|
||||
@@ -303,6 +336,7 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
||||
result: payload.result,
|
||||
error: payload.error,
|
||||
duration: payload.duration,
|
||||
fileDiff: fileDiffInfo,
|
||||
} as ToolMessagePart;
|
||||
}
|
||||
return part;
|
||||
|
||||
@@ -201,6 +201,8 @@ export type {
|
||||
DiagnosticsResponse,
|
||||
// Active file types
|
||||
ActiveFileInfo,
|
||||
// File diff types
|
||||
FileDiffInfo,
|
||||
} from './api/client.js';
|
||||
|
||||
// Primitives (shadcn/ui style)
|
||||
@@ -254,7 +256,8 @@ export { DiagnosticsIndicator, DiagnosticsIndicatorCompact } from './components/
|
||||
export { SessionPanel } from './components/SessionPanel.js';
|
||||
export { FileExplorer } from './components/FileExplorer.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 { Resizer } from './components/Resizer.js';
|
||||
export { ToolbarOverflowMenu, type ToolbarMenuItem } from './components/ToolbarOverflowMenu.js';
|
||||
|
||||
Reference in New Issue
Block a user