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)
+17 -1
View File
@@ -23,6 +23,7 @@ import {
listSessions,
createSession,
type Session,
type ActiveFileInfo,
} from '@ai-assistant/ui';
import { ChatPage } from './pages/Chat';
@@ -45,6 +46,18 @@ export function App() {
return saved ? parseFloat(saved) : 70;
});
// 编辑器联动状态
const [activeFile, setActiveFile] = useState<ActiveFileInfo | null>(null);
const [autoAttachActiveFile, setAutoAttachActiveFile] = useState(() => {
const saved = localStorage.getItem('ai-assistant-auto-attach-file');
return saved !== 'false'; // 默认开启
});
// 持久化自动附加开关状态
useEffect(() => {
localStorage.setItem('ai-assistant-auto-attach-file', String(autoAttachActiveFile));
}, [autoAttachActiveFile]);
// 初始化:加载会话
useEffect(() => {
const HAS_SESSIONS_KEY = 'ai-assistant-has-sessions';
@@ -146,7 +159,7 @@ export function App() {
className="hidden md:flex flex-col"
style={{ width: `${idePanelWidth}%` }}
>
<IDE />
<IDE onActiveFileChange={setActiveFile} />
</div>
{/* 可拖拽分割线 */}
@@ -175,6 +188,9 @@ export function App() {
onOpenLSP={() => setShowLSP(true)}
onOpenDiagnostics={() => setShowDiagnostics(true)}
onOpenSessions={() => setShowSessions(true)}
activeFile={activeFile}
autoAttachActiveFile={autoAttachActiveFile}
onAutoAttachActiveFileToggle={setAutoAttachActiveFile}
/>
) : (
<div className="flex-1 flex items-center justify-center h-full">
+14
View File
@@ -16,6 +16,7 @@ import {
SubagentProgress,
DiagnosticsIndicator,
ToolbarOverflowMenu,
type ActiveFileInfo,
} from '@ai-assistant/ui';
interface ChatPageProps {
@@ -35,6 +36,13 @@ interface ChatPageProps {
onOpenLSP?: () => void;
onOpenDiagnostics?: () => void;
onOpenSessions?: () => void;
// 编辑器联动
/** 当前编辑器活动文件 */
activeFile?: ActiveFileInfo | null;
/** 是否自动附加当前编辑器文件 */
autoAttachActiveFile?: boolean;
/** 自动附加开关变更回调 */
onAutoAttachActiveFileToggle?: (enabled: boolean) => void;
}
export function ChatPage({
@@ -52,6 +60,9 @@ export function ChatPage({
onOpenLSP,
onOpenDiagnostics,
onOpenSessions,
activeFile,
autoAttachActiveFile,
onAutoAttachActiveFileToggle,
}: ChatPageProps) {
const {
messages,
@@ -228,6 +239,9 @@ export function ChatPage({
onAgentModeChange={setAgentMode}
autoApprove={autoApprove}
onAutoApproveChange={setAutoApprove}
activeFile={activeFile}
autoAttachActiveFile={autoAttachActiveFile}
onAutoAttachActiveFileToggle={onAutoAttachActiveFileToggle}
/>
{/* Permission Dialog */}