packages/desktop/src/renderer/pages/conversation/Workspace/README.cn.md
Workspace 模块是 AionUi 中用于管理对话工作空间文件和文件夹的核心组件。它提供了完整的文件树展示、文件操作(打开、删除、重命名、预览)、文件添加和粘贴等功能。该模块采用 React Hooks 架构,将业务逻辑拆分为多个独立的 Hook,实现了高度模块化和可维护性。
Workspace 模块遵循容器组件模式(Container Component Pattern):
这种架构的优势:
workspace/
├── index.tsx # 容器组件 (550行) - 组合所有 Hook
├── hooks/ # 业务逻辑 Hooks
│ ├── useWorkspaceTree.ts # 树状态管理和选择逻辑
│ ├── useWorkspaceEvents.ts # 事件监听器管理
│ ├── useWorkspaceFileOps.ts # 文件操作(打开、删除、重命名、预览)
│ ├── useWorkspaceModals.ts # 模态框和菜单状态管理
│ └── useWorkspacePaste.ts # 文件粘贴和添加逻辑
├── utils/
│ └── treeHelpers.ts # 树结构操作工具函数
└── types.ts # TypeScript 类型定义
职责: 管理工作空间文件树的状态和选择逻辑
主要功能:
核心 API:
const {
// 状态
files, // 文件树数据
loading, // 加载状态(带防抖)
selected, // 选中的节点 keys
expandedKeys, // 展开的节点 keys
selectedNodeRef, // 最后选中的文件夹节点引用
// 操作
loadWorkspace, // 加载工作空间
refreshWorkspace, // 刷新工作空间
ensureNodeSelected, // 确保节点被选中
clearSelection, // 清空选择
} = useWorkspaceTree({ workspace, conversation_id, eventPrefix });
特性:
职责: 管理所有事件监听器
监听的事件:
tool_group, tool_callacp_tool_call${eventPrefix}.workspace.refresh${eventPrefix}.selected.file.clear(发送消息后)特性:
职责: 处理所有文件操作逻辑
主要功能:
handleOpenNode) - 使用系统默认程序打开文件/文件夹handleRevealNode) - 在系统文件管理器中显示handleDeleteNode, handleDeleteConfirm) - 带确认的删除操作openRenameModal, handleRenameConfirm) - 带超时保护的重命名handlePreviewFile) - 支持多种格式预览handleAddToChat) - 将文件/文件夹添加到对话支持的预览格式:
.md, .markdown.diff, .patch.pdf, .ppt, .pptx, .doc, .docx, .xls, .xlsx, .csv.js, .ts, .tsx, .jsx, .py, .java, .go, .rs, .c, .cpp, .json, .xml, .yaml 等.png, .jpg, .jpeg, .gif, .bmp, .webp, .svg, .ico 等.html, .htm特性:
职责: 管理所有模态框和菜单状态
管理的状态:
contextMenu) - 位置、可见性、目标节点renameModal) - 可见性、输入值、目标节点、加载状态deleteModal) - 可见性、目标节点、加载状态pasteConfirm) - 可见性、文件列表、"不再询问"选项核心 API:
const {
// 右键菜单
contextMenu,
openContextMenu,
closeContextMenu,
// 重命名弹窗
renameModal,
setRenameModal,
renameLoading,
closeRenameModal,
// 删除弹窗
deleteModal,
setDeleteModal,
closeDeleteModal,
// 粘贴确认
pasteConfirm,
setPasteConfirm,
closePasteConfirm,
} = useWorkspaceModals();
特性:
职责: 处理文件粘贴和添加逻辑
主要功能:
handleAddFiles) - 从文件选择器添加handleFilesToAdd) - 从系统粘贴板添加handlePasteConfirm) - 处理粘贴确认对话框工作流程:
用户粘贴文件
↓
检查 workspace.pasteConfirm 配置
↓
├─ 已禁用确认 → 直接复制到目标文件夹
└─ 需要确认 → 显示确认对话框
↓
用户确认 → 复制文件
↓
如果勾选"不再询问" → 保存配置
特性:
usePasteService 捕获全局粘贴事件workspace.pasteConfirm)import ChatWorkspace from './workspace';
function ConversationPage() {
const [messageApi, messageContext] = Message.useMessage();
return (
<>
{messageContext}
<ChatWorkspace
conversation_id={conversationId}
workspace={workspacePath}
eventPrefix='gemini'
messageApi={messageApi}
/>
</>
);
}
import { emitter } from '@/renderer/utils/emitter';
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
const handleFileSelected = (items: Array<{ path: string; name: string; isFile: boolean }>) => {
console.log('选中的文件:', items);
};
emitter.on('gemini.selected.file', handleFileSelected);
return () => {
emitter.off('gemini.selected.file', handleFileSelected);
};
}, []);
}
import { emitter } from '@/renderer/utils/emitter';
function RefreshButton() {
const handleRefresh = () => {
emitter.emit('gemini.workspace.refresh');
};
return <button onClick={handleRefresh}>刷新</button>;
}
import { emitter } from '@/renderer/utils/emitter';
function ClearButton() {
const handleClear = () => {
emitter.emit('gemini.selected.file.clear');
};
return <button onClick={handleClear}>清空选择</button>;
}
eventPrefix 用于区分不同的 Agent 类型,支持:
gemini - Gemini AI 对话acp - ACP (AI Code Partner) 对话codex - Codex 对话事件命名规则: ${eventPrefix}.${eventName}
Workspace 依赖 PreviewContext 来实现文件预览:
import { PreviewProvider } from '../preview';
function Layout() {
return (
<PreviewProvider>
<ChatWorkspace {...props} />
</PreviewProvider>
);
}
通过 workspace.pasteConfirm 配置项控制是否显示粘贴确认对话框:
// 禁用粘贴确认
await ConfigStorage.set('workspace.pasteConfirm', true);
// 启用粘贴确认(默认)
await ConfigStorage.set('workspace.pasteConfirm', false);
Loading 图标至少保持1秒,避免快速切换造成的闪烁:
if (Date.now() - lastLoadingTime.current > 1000) {
setLoading(false);
} else {
setTimeout(() => setLoading(false), 1000);
}
搜索输入使用 useDebounce Hook,延迟200ms执行,减少不必要的请求:
const onSearch = useDebounce(
(value: string) => {
void treeHook.loadWorkspace(workspace, value);
},
200,
[workspace, treeHook.loadWorkspace]
);
使用 useRef 存储选择状态,避免不必要的重渲染:
const selectedKeysRef = useRef<string[]>([]);
const selectedNodeRef = useRef<SelectedNodeRef | null>(null);
所有文件操作都包含完整的错误处理:
try {
const result = await operation();
if (!result.success) {
messageApi.error(result.msg || t('defaultErrorMessage'));
}
} catch (error) {
messageApi.error(t('unknownError'));
}
index.tsx (容器组件)
↓
├── useWorkspaceTree (独立)
├── useWorkspaceModals (独立)
├── useWorkspacePaste (依赖: Tree, Modals)
├── useWorkspaceFileOps (依赖: Tree, Modals, Preview)
└── useWorkspaceEvents (依赖: Tree, Modals)
在 useWorkspaceFileOps 的 handlePreviewFile 函数中添加新的扩展名判断:
if (['new', 'ext'].includes(ext)) {
contentType = 'newType';
}
修改 index.tsx 中的右键菜单渲染逻辑(第363-429行)。
延迟200ms是为了确保文件系统操作完成,避免刷新时读取到旧数据。
用户可以在粘贴确认对话框中勾选"不再询问",或者通过代码设置:
await ConfigStorage.set('workspace.pasteConfirm', true);