feat(ui): 添加编辑器与对话框联动功能

- 新增 ActiveFileInfo 类型定义
- IDE 组件支持 onActiveFileChange 回调通知活动文件变化
- ChatInput 显示当前活动文件并支持自动附加到消息
- 用户可切换自动附加开关,设置持久化到 localStorage
- 排除 / 和 : 命令避免与斜杠命令和系统命令冲突
This commit is contained in:
2025-12-17 19:59:13 +08:00
parent 48a11ff077
commit 3320a2a5ba
7 changed files with 133 additions and 7 deletions
+2
View File
@@ -151,6 +151,8 @@ export type {
// System Commands types
SystemCommandInfo,
SystemCommandListResponse,
// Active file types
ActiveFileInfo,
} from './types.js';
// API Configuration
+14
View File
@@ -1144,3 +1144,17 @@ export interface AllFilesDiagnosticsResponse {
/** 诊断响应(联合类型) */
export type DiagnosticsResponse = SingleFileDiagnosticsResponse | AllFilesDiagnosticsResponse;
// ============ 编辑器活动文件相关 ============
/** 当前编辑器活动文件信息 */
export interface ActiveFileInfo {
/** 文件路径 */
path: string;
/** 文件名 */
name: string;
/** 文件内容 */
content: string;
/** 语言类型 */
language: string;
}
+63 -5
View File
@@ -8,7 +8,7 @@
*/
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { Square, Sparkles } from 'lucide-react';
import { Square, Sparkles, FileCode, Link } from 'lucide-react';
import { motion } from 'framer-motion';
import clsx from 'clsx';
import { CommandMenu, type CommandMenuItem } from './CommandMenu.js';
@@ -19,6 +19,7 @@ import { AgentModeSelector, type AgentModeType } from './AgentModeSelector.js';
import { useCommands } from '../hooks/useCommands.js';
import { useSystemCommands } from '../hooks/useSystemCommands.js';
import { useFileMention } from '../hooks/useFileMention.js';
import type { ActiveFileInfo } from '../api/types.js';
interface ChatInputProps {
onSend: (content: string) => void;
@@ -39,6 +40,12 @@ interface ChatInputProps {
autoApprove?: boolean;
/** 自动授权变更回调 */
onAutoApproveChange?: (enabled: boolean) => void;
/** 当前编辑器活动文件 */
activeFile?: ActiveFileInfo | null;
/** 是否自动附加当前编辑器文件 */
autoAttachActiveFile?: boolean;
/** 自动附加开关变更回调 */
onAutoAttachActiveFileToggle?: (enabled: boolean) => void;
}
export function ChatInput({
@@ -53,6 +60,9 @@ export function ChatInput({
onAgentModeChange,
autoApprove = false,
onAutoApproveChange,
activeFile,
autoAttachActiveFile = true,
onAutoAttachActiveFileToggle,
}: ChatInputProps) {
const [input, setInput] = useState('');
const [showCommandMenu, setShowCommandMenu] = useState(false);
@@ -249,7 +259,17 @@ export function ChatInput({
setShowSystemCommandMenu(false);
closeFileMenu();
onSend(trimmed);
// 构建最终消息内容
let messageContent = trimmed;
// 自动附加当前编辑器文件(如果启用且文件未在 @提及中)
// 排除 / 斜杠命令和 : 系统命令
const isCommand = trimmed.startsWith('/') || trimmed.startsWith(':');
if (!isCommand && autoAttachActiveFile && activeFile && !mentionedFiles.includes(activeFile.path)) {
messageContent = `@${activeFile.path} ${messageContent}`;
}
onSend(messageContent);
setInput('');
// 重置高度
@@ -370,9 +390,47 @@ export function ChatInput({
: 'border-line hover:border-fg-subtle/30 focus-within:border-primary-500 focus-within:shadow-lg focus-within:shadow-primary-500/10'
)}
>
{/* 已选文件标签 */}
{mentionedFiles.length > 0 && (
<div className="flex flex-wrap gap-1.5 px-4 pt-3">
{/* 文件标签区域:活动文件 + 已选文件 */}
{(activeFile || mentionedFiles.length > 0) && (
<div className="flex flex-wrap items-center gap-1.5 px-4 pt-3">
{/* 当前编辑器活动文件 */}
{activeFile && !mentionedFiles.includes(activeFile.path) && (
<div className="flex items-center gap-1">
{/* 自动附加开关 */}
{onAutoAttachActiveFileToggle && (
<button
type="button"
onClick={() => onAutoAttachActiveFileToggle(!autoAttachActiveFile)}
className={clsx(
'p-1 rounded transition-colors',
autoAttachActiveFile
? 'text-cyan-500 hover:text-cyan-400'
: 'text-fg-subtle hover:text-fg-muted'
)}
title={autoAttachActiveFile ? '点击取消自动附加' : '点击启用自动附加'}
>
<Link size={14} />
</button>
)}
{/* 活动文件标签 */}
<div
className={clsx(
'inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-xs transition-colors',
autoAttachActiveFile
? 'bg-cyan-500/15 text-cyan-400 border border-cyan-500/30'
: 'bg-surface-muted text-fg-muted border border-transparent'
)}
title={autoAttachActiveFile ? `当前文件: ${activeFile.path}(将自动附加)` : `当前文件: ${activeFile.path}(不会附加)`}
>
<FileCode size={12} />
<span className="max-w-[120px] truncate">{activeFile.name}</span>
{autoAttachActiveFile && (
<span className="text-[10px] opacity-70">(auto)</span>
)}
</div>
</div>
)}
{/* @提及的文件 */}
{mentionedFiles.map((file, index) => (
<FileMentionTag
key={`${file}-${index}`}
+21 -1
View File
@@ -10,6 +10,7 @@ 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';
// localStorage 键名
const STORAGE_KEY_TABS = 'ai-assistant-editor-tabs';
@@ -25,9 +26,11 @@ interface IDEProps {
className?: string;
/** 文件浏览器宽度 */
sidebarWidth?: number;
/** 当前活动文件变化回调 */
onActiveFileChange?: (file: ActiveFileInfo | null) => void;
}
export function IDE({ className, sidebarWidth = 256 }: IDEProps) {
export function IDE({ className, sidebarWidth = 256, onActiveFileChange }: IDEProps) {
const [tabs, setTabs] = useState<EditorTab[]>([]);
const [activeTabId, setActiveTabId] = useState<string | null>(null);
const [workingDirectory, setWorkingDirectory] = useState<string>('');
@@ -134,6 +137,23 @@ export function IDE({ className, sidebarWidth = 256 }: IDEProps) {
}
}, [activeTabId, tabs]);
// 通知父组件活动文件变化
useEffect(() => {
if (!onActiveFileChange) return;
const activeTab = tabs.find((tab) => tab.id === activeTabId);
if (activeTab) {
onActiveFileChange({
path: activeTab.path,
name: activeTab.name,
content: activeTab.content,
language: activeTab.language,
});
} else {
onActiveFileChange(null);
}
}, [activeTabId, tabs, onActiveFileChange]);
// 打开文件
const handleFileSelect = useCallback(async (path: string, name: string) => {
// 检查是否已打开
+2
View File
@@ -199,6 +199,8 @@ export type {
SingleFileDiagnosticsResponse,
AllFilesDiagnosticsResponse,
DiagnosticsResponse,
// Active file types
ActiveFileInfo,
} from './api/client.js';
// Primitives (shadcn/ui style)