feat(ui): 添加编辑器与对话框联动功能
- 新增 ActiveFileInfo 类型定义 - IDE 组件支持 onActiveFileChange 回调通知活动文件变化 - ChatInput 显示当前活动文件并支持自动附加到消息 - 用户可切换自动附加开关,设置持久化到 localStorage - 排除 / 和 : 命令避免与斜杠命令和系统命令冲突
This commit is contained in:
@@ -151,6 +151,8 @@ export type {
|
||||
// System Commands types
|
||||
SystemCommandInfo,
|
||||
SystemCommandListResponse,
|
||||
// Active file types
|
||||
ActiveFileInfo,
|
||||
} from './types.js';
|
||||
|
||||
// API Configuration
|
||||
|
||||
@@ -1144,3 +1144,17 @@ export interface AllFilesDiagnosticsResponse {
|
||||
/** 诊断响应(联合类型) */
|
||||
export type DiagnosticsResponse = SingleFileDiagnosticsResponse | AllFilesDiagnosticsResponse;
|
||||
|
||||
// ============ 编辑器活动文件相关 ============
|
||||
|
||||
/** 当前编辑器活动文件信息 */
|
||||
export interface ActiveFileInfo {
|
||||
/** 文件路径 */
|
||||
path: string;
|
||||
/** 文件名 */
|
||||
name: string;
|
||||
/** 文件内容 */
|
||||
content: string;
|
||||
/** 语言类型 */
|
||||
language: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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) => {
|
||||
// 检查是否已打开
|
||||
|
||||
@@ -199,6 +199,8 @@ export type {
|
||||
SingleFileDiagnosticsResponse,
|
||||
AllFilesDiagnosticsResponse,
|
||||
DiagnosticsResponse,
|
||||
// Active file types
|
||||
ActiveFileInfo,
|
||||
} from './api/client.js';
|
||||
|
||||
// Primitives (shadcn/ui style)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user