feat(ui): 添加编辑器与对话框联动功能
- 新增 ActiveFileInfo 类型定义 - IDE 组件支持 onActiveFileChange 回调通知活动文件变化 - ChatInput 显示当前活动文件并支持自动附加到消息 - 用户可切换自动附加开关,设置持久化到 localStorage - 排除 / 和 : 命令避免与斜杠命令和系统命令冲突
This commit is contained in:
@@ -151,6 +151,8 @@ export type {
|
|||||||
// System Commands types
|
// System Commands types
|
||||||
SystemCommandInfo,
|
SystemCommandInfo,
|
||||||
SystemCommandListResponse,
|
SystemCommandListResponse,
|
||||||
|
// Active file types
|
||||||
|
ActiveFileInfo,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
// API Configuration
|
// API Configuration
|
||||||
|
|||||||
@@ -1144,3 +1144,17 @@ export interface AllFilesDiagnosticsResponse {
|
|||||||
/** 诊断响应(联合类型) */
|
/** 诊断响应(联合类型) */
|
||||||
export type DiagnosticsResponse = SingleFileDiagnosticsResponse | 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 { 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 { motion } from 'framer-motion';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { CommandMenu, type CommandMenuItem } from './CommandMenu.js';
|
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 { useCommands } from '../hooks/useCommands.js';
|
||||||
import { useSystemCommands } from '../hooks/useSystemCommands.js';
|
import { useSystemCommands } from '../hooks/useSystemCommands.js';
|
||||||
import { useFileMention } from '../hooks/useFileMention.js';
|
import { useFileMention } from '../hooks/useFileMention.js';
|
||||||
|
import type { ActiveFileInfo } from '../api/types.js';
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
onSend: (content: string) => void;
|
onSend: (content: string) => void;
|
||||||
@@ -39,6 +40,12 @@ interface ChatInputProps {
|
|||||||
autoApprove?: boolean;
|
autoApprove?: boolean;
|
||||||
/** 自动授权变更回调 */
|
/** 自动授权变更回调 */
|
||||||
onAutoApproveChange?: (enabled: boolean) => void;
|
onAutoApproveChange?: (enabled: boolean) => void;
|
||||||
|
/** 当前编辑器活动文件 */
|
||||||
|
activeFile?: ActiveFileInfo | null;
|
||||||
|
/** 是否自动附加当前编辑器文件 */
|
||||||
|
autoAttachActiveFile?: boolean;
|
||||||
|
/** 自动附加开关变更回调 */
|
||||||
|
onAutoAttachActiveFileToggle?: (enabled: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatInput({
|
export function ChatInput({
|
||||||
@@ -53,6 +60,9 @@ export function ChatInput({
|
|||||||
onAgentModeChange,
|
onAgentModeChange,
|
||||||
autoApprove = false,
|
autoApprove = false,
|
||||||
onAutoApproveChange,
|
onAutoApproveChange,
|
||||||
|
activeFile,
|
||||||
|
autoAttachActiveFile = true,
|
||||||
|
onAutoAttachActiveFileToggle,
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [showCommandMenu, setShowCommandMenu] = useState(false);
|
const [showCommandMenu, setShowCommandMenu] = useState(false);
|
||||||
@@ -249,7 +259,17 @@ export function ChatInput({
|
|||||||
setShowSystemCommandMenu(false);
|
setShowSystemCommandMenu(false);
|
||||||
closeFileMenu();
|
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('');
|
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'
|
: '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 && (
|
{(activeFile || mentionedFiles.length > 0) && (
|
||||||
<div className="flex flex-wrap gap-1.5 px-4 pt-3">
|
<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) => (
|
{mentionedFiles.map((file, index) => (
|
||||||
<FileMentionTag
|
<FileMentionTag
|
||||||
key={`${file}-${index}`}
|
key={`${file}-${index}`}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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';
|
||||||
|
|
||||||
// localStorage 键名
|
// localStorage 键名
|
||||||
const STORAGE_KEY_TABS = 'ai-assistant-editor-tabs';
|
const STORAGE_KEY_TABS = 'ai-assistant-editor-tabs';
|
||||||
@@ -25,9 +26,11 @@ interface IDEProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
/** 文件浏览器宽度 */
|
/** 文件浏览器宽度 */
|
||||||
sidebarWidth?: number;
|
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 [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>('');
|
||||||
@@ -134,6 +137,23 @@ export function IDE({ className, sidebarWidth = 256 }: IDEProps) {
|
|||||||
}
|
}
|
||||||
}, [activeTabId, tabs]);
|
}, [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) => {
|
const handleFileSelect = useCallback(async (path: string, name: string) => {
|
||||||
// 检查是否已打开
|
// 检查是否已打开
|
||||||
|
|||||||
@@ -199,6 +199,8 @@ export type {
|
|||||||
SingleFileDiagnosticsResponse,
|
SingleFileDiagnosticsResponse,
|
||||||
AllFilesDiagnosticsResponse,
|
AllFilesDiagnosticsResponse,
|
||||||
DiagnosticsResponse,
|
DiagnosticsResponse,
|
||||||
|
// Active file types
|
||||||
|
ActiveFileInfo,
|
||||||
} from './api/client.js';
|
} from './api/client.js';
|
||||||
|
|
||||||
// Primitives (shadcn/ui style)
|
// Primitives (shadcn/ui style)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
listSessions,
|
listSessions,
|
||||||
createSession,
|
createSession,
|
||||||
type Session,
|
type Session,
|
||||||
|
type ActiveFileInfo,
|
||||||
} from '@ai-assistant/ui';
|
} from '@ai-assistant/ui';
|
||||||
import { ChatPage } from './pages/Chat';
|
import { ChatPage } from './pages/Chat';
|
||||||
|
|
||||||
@@ -45,6 +46,18 @@ export function App() {
|
|||||||
return saved ? parseFloat(saved) : 70;
|
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(() => {
|
useEffect(() => {
|
||||||
const HAS_SESSIONS_KEY = 'ai-assistant-has-sessions';
|
const HAS_SESSIONS_KEY = 'ai-assistant-has-sessions';
|
||||||
@@ -146,7 +159,7 @@ export function App() {
|
|||||||
className="hidden md:flex flex-col"
|
className="hidden md:flex flex-col"
|
||||||
style={{ width: `${idePanelWidth}%` }}
|
style={{ width: `${idePanelWidth}%` }}
|
||||||
>
|
>
|
||||||
<IDE />
|
<IDE onActiveFileChange={setActiveFile} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 可拖拽分割线 */}
|
{/* 可拖拽分割线 */}
|
||||||
@@ -175,6 +188,9 @@ export function App() {
|
|||||||
onOpenLSP={() => setShowLSP(true)}
|
onOpenLSP={() => setShowLSP(true)}
|
||||||
onOpenDiagnostics={() => setShowDiagnostics(true)}
|
onOpenDiagnostics={() => setShowDiagnostics(true)}
|
||||||
onOpenSessions={() => setShowSessions(true)}
|
onOpenSessions={() => setShowSessions(true)}
|
||||||
|
activeFile={activeFile}
|
||||||
|
autoAttachActiveFile={autoAttachActiveFile}
|
||||||
|
onAutoAttachActiveFileToggle={setAutoAttachActiveFile}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex items-center justify-center h-full">
|
<div className="flex-1 flex items-center justify-center h-full">
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
SubagentProgress,
|
SubagentProgress,
|
||||||
DiagnosticsIndicator,
|
DiagnosticsIndicator,
|
||||||
ToolbarOverflowMenu,
|
ToolbarOverflowMenu,
|
||||||
|
type ActiveFileInfo,
|
||||||
} from '@ai-assistant/ui';
|
} from '@ai-assistant/ui';
|
||||||
|
|
||||||
interface ChatPageProps {
|
interface ChatPageProps {
|
||||||
@@ -35,6 +36,13 @@ interface ChatPageProps {
|
|||||||
onOpenLSP?: () => void;
|
onOpenLSP?: () => void;
|
||||||
onOpenDiagnostics?: () => void;
|
onOpenDiagnostics?: () => void;
|
||||||
onOpenSessions?: () => void;
|
onOpenSessions?: () => void;
|
||||||
|
// 编辑器联动
|
||||||
|
/** 当前编辑器活动文件 */
|
||||||
|
activeFile?: ActiveFileInfo | null;
|
||||||
|
/** 是否自动附加当前编辑器文件 */
|
||||||
|
autoAttachActiveFile?: boolean;
|
||||||
|
/** 自动附加开关变更回调 */
|
||||||
|
onAutoAttachActiveFileToggle?: (enabled: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatPage({
|
export function ChatPage({
|
||||||
@@ -52,6 +60,9 @@ export function ChatPage({
|
|||||||
onOpenLSP,
|
onOpenLSP,
|
||||||
onOpenDiagnostics,
|
onOpenDiagnostics,
|
||||||
onOpenSessions,
|
onOpenSessions,
|
||||||
|
activeFile,
|
||||||
|
autoAttachActiveFile,
|
||||||
|
onAutoAttachActiveFileToggle,
|
||||||
}: ChatPageProps) {
|
}: ChatPageProps) {
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
@@ -228,6 +239,9 @@ export function ChatPage({
|
|||||||
onAgentModeChange={setAgentMode}
|
onAgentModeChange={setAgentMode}
|
||||||
autoApprove={autoApprove}
|
autoApprove={autoApprove}
|
||||||
onAutoApproveChange={setAutoApprove}
|
onAutoApproveChange={setAutoApprove}
|
||||||
|
activeFile={activeFile}
|
||||||
|
autoAttachActiveFile={autoAttachActiveFile}
|
||||||
|
onAutoAttachActiveFileToggle={onAutoAttachActiveFileToggle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Permission Dialog */}
|
{/* Permission Dialog */}
|
||||||
|
|||||||
Reference in New Issue
Block a user