diff --git a/apps/electron/src/main/ipc.ts b/apps/electron/src/main/ipc.ts index e7e15b154..2ed1669e9 100644 --- a/apps/electron/src/main/ipc.ts +++ b/apps/electron/src/main/ipc.ts @@ -2950,11 +2950,11 @@ export function registerIpcHandlers(): void { } ) - // 打开支持文件与文件夹混合选择的 Composer 对话框 + // 打开文件或文件夹选择对话框(类型由渲染层 Dialog 决定) ipcMain.handle( AGENT_IPC_CHANNELS.OPEN_FILE_OR_FOLDER_DIALOG, - async (): Promise => { - return openFileOrFolderDialog() + async (_, type: 'file' | 'folder'): Promise => { + return openFileOrFolderDialog(type) } ) diff --git a/apps/electron/src/main/lib/attachment-service.ts b/apps/electron/src/main/lib/attachment-service.ts index 198d6aac7..75203a6a5 100644 --- a/apps/electron/src/main/lib/attachment-service.ts +++ b/apps/electron/src/main/lib/attachment-service.ts @@ -340,37 +340,22 @@ export async function openFileDialog(): Promise { } /** - * 打开 Agent Composer 的混合选择对话框。 - * 文件按附件语义读取,文件夹只返回路径,交由会话目录授权流程处理。 + * 打开 Agent Composer 的内容选择对话框。 + * 类型选择由渲染层用 Proma 自身 Dialog 完成(见 AttachmentContentDialog), + * 此处按调用方指定的类型直接打开对应系统选择器: + * - 'file':文件选择器,文件按附件语义读取 + * - 'folder':文件夹选择器,目录只返回路径,交由会话目录授权流程处理 */ -export async function openFileOrFolderDialog(): Promise { +export async function openFileOrFolderDialog(type: 'file' | 'folder'): Promise { + // 运行时守卫:非法类型直接拒绝,避免被静默当作 'folder' 处理 + if (type !== 'file' && type !== 'folder') { + throw new Error(`无效的附加内容类型: ${String(type)}`) + } const parentWindow = BrowserWindow.getFocusedWindow() - let dialogOptions: Electron.OpenDialogOptions - - if (process.platform === 'win32' || process.platform === 'linux') { - const choiceOptions: Electron.MessageBoxOptions = { - type: 'question', - title: '附加文件或文件夹', - message: '请选择要附加的内容类型', - buttons: ['文件', '文件夹', '取消'], - defaultId: 0, - cancelId: 2, - noLink: true, - } - const choice = parentWindow - ? await dialog.showMessageBox(parentWindow, choiceOptions) - : await dialog.showMessageBox(choiceOptions) - if (choice.response === 2) return { files: [], directories: [] } - - dialogOptions = choice.response === 0 + const dialogOptions: Electron.OpenDialogOptions = + type === 'file' ? { properties: ['openFile', 'multiSelections'], filters: FILE_FILTERS, title: '附加文件' } : { properties: ['openDirectory', 'multiSelections'], title: '附加文件夹' } - } else { - dialogOptions = { - properties: ['openFile', 'openDirectory', 'multiSelections'], - title: '附加文件或文件夹', - } - } const result = parentWindow ? await dialog.showOpenDialog(parentWindow, dialogOptions) @@ -382,6 +367,6 @@ export async function openFileOrFolderDialog(): Promise Promise<{ path: string; name: string } | null> - /** 打开支持文件与文件夹混合选择的 Composer 对话框 */ - openFileOrFolderDialog: () => Promise + /** 打开支持文件与文件夹混合选择的 Composer 对话框(类型由渲染层 Dialog 决定) */ + openFileOrFolderDialog: (type: 'file' | 'folder') => Promise /** 附加外部目录到 Agent 会话 */ attachDirectory: (input: AgentAttachDirectoryInput) => Promise @@ -2031,8 +2031,8 @@ const electronAPI: ElectronAPI = { return ipcRenderer.invoke(AGENT_IPC_CHANNELS.OPEN_FOLDER_DIALOG) }, - openFileOrFolderDialog: () => { - return ipcRenderer.invoke(AGENT_IPC_CHANNELS.OPEN_FILE_OR_FOLDER_DIALOG) + openFileOrFolderDialog: (type: 'file' | 'folder') => { + return ipcRenderer.invoke(AGENT_IPC_CHANNELS.OPEN_FILE_OR_FOLDER_DIALOG, type) }, attachDirectory: (input: AgentAttachDirectoryInput) => { diff --git a/apps/electron/src/renderer/components/agent/AgentView.tsx b/apps/electron/src/renderer/components/agent/AgentView.tsx index 6aee85098..c13d8b18a 100644 --- a/apps/electron/src/renderer/components/agent/AgentView.tsx +++ b/apps/electron/src/renderer/components/agent/AgentView.tsx @@ -19,6 +19,7 @@ import { useAtom, useAtomValue, useSetAtom, useStore } from 'jotai' import { toast } from 'sonner' import { Box, CornerDownLeft, Square, Settings, X, Copy, Check, Brain, Sparkles, ChevronDown, ListTodo, Paperclip } from 'lucide-react' import { AgentMessages } from './AgentMessages' +import { AttachmentContentDialog } from './AttachmentContentDialog' import { AgentHeader } from './AgentHeader' import { AgentMessageQueue } from './AgentMessageQueue' import { ContextUsageBadge } from './ContextUsageBadge' @@ -1706,10 +1707,23 @@ export function AgentView({ sessionId }: { sessionId: string }): React.ReactElem } }, [addLargeDialogFilesAsReferences, setPendingFiles]) - /** 打开混合选择器:文件作为附件,文件夹仅授权给当前会话。 */ - const handleAttachContent = React.useCallback(async (): Promise => { + /** Composer 附加内容类型选择弹窗(替代系统原生 MessageBox)是否打开 */ + const [attachContentDialogOpen, setAttachContentDialogOpen] = React.useState(false) + /** 重入锁:防止退出动画期间双击“文件/文件夹”重复打开系统选择器 */ + const attachContentBusyRef = React.useRef(false) + + /** 打开混合选择器:先弹 Proma 自身类型选择弹窗,文件作为附件,文件夹仅授权给当前会话。 */ + const handleAttachContent = React.useCallback((): void => { + setAttachContentDialogOpen(true) + }, []) + + /** 按用户选择的类型打开系统选择器,并复用原有附件/目录处理逻辑。 */ + const handleAttachContentType = React.useCallback(async (type: 'file' | 'folder'): Promise => { + if (attachContentBusyRef.current) return + attachContentBusyRef.current = true + setAttachContentDialogOpen(false) try { - const result = await window.electronAPI.openFileOrFolderDialog() + const result = await window.electronAPI.openFileOrFolderDialog(type) const largeFiles = result.largeFiles ?? [] const skippedFiles = result.skippedFiles ?? [] if (result.files.length === 0 && largeFiles.length === 0 && skippedFiles.length === 0 && result.directories.length === 0) return @@ -1745,6 +1759,8 @@ export function AgentView({ sessionId }: { sessionId: string }): React.ReactElem } catch (error) { console.error('[AgentView] 附加内容选择失败:', error) toast.error('附加文件或文件夹失败') + } finally { + attachContentBusyRef.current = false } }, [addDialogFilesAsAttachments, sessionId, setAttachedDirsMap]) @@ -2964,7 +2980,7 @@ export function AgentView({ sessionId }: { sessionId: string }): React.ReactElem variant="ghost" size="icon" className={inputToolbarButtonClass} - onClick={() => void handleAttachContent()} + onClick={handleAttachContent} aria-label="附加文件或文件夹" > @@ -3265,6 +3281,12 @@ export function AgentView({ sessionId }: { sessionId: string }): React.ReactElem + void handleAttachContentType(type)} + /> + 标记为 Todo diff --git a/apps/electron/src/renderer/components/agent/AttachmentContentDialog.tsx b/apps/electron/src/renderer/components/agent/AttachmentContentDialog.tsx new file mode 100644 index 000000000..98cc4142b --- /dev/null +++ b/apps/electron/src/renderer/components/agent/AttachmentContentDialog.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import { FileText, Folder } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' + +interface AttachmentContentDialogProps { + /** 是否显示弹窗(受控) */ + open: boolean + /** 打开状态变化回调(取消/关闭时传 false) */ + onOpenChange: (open: boolean) => void + /** 用户选择附加类型后回调 */ + onSelect: (type: 'file' | 'folder') => void +} + +/** + * Composer 附加内容类型选择弹窗。 + * 替代原先主进程的系统原生 MessageBox,保持 Proma 自身 UI 风格。 + */ +export function AttachmentContentDialog({ + open, + onOpenChange, + onSelect, +}: AttachmentContentDialogProps): React.ReactElement { + return ( + + + + 附加文件或文件夹 + 请选择要附加的内容类型 + + + + {/* 文件为默认操作:焦点(autoFocus)、Enter 默认触发、视觉主按钮三者统一 */} + + + + + + ) +} diff --git a/packages/shared/src/types/agent.ts b/packages/shared/src/types/agent.ts index b2f78d353..6e4665f31 100644 --- a/packages/shared/src/types/agent.ts +++ b/packages/shared/src/types/agent.ts @@ -1652,7 +1652,7 @@ export const AGENT_IPC_CHANNELS = { GET_WORKSPACE_FILES_PATH: 'agent:get-workspace-files-path', /** 打开文件夹选择对话框 */ OPEN_FOLDER_DIALOG: 'agent:open-folder-dialog', - /** 打开支持文件与文件夹混合选择的 Composer 对话框 */ + /** 打开文件或文件夹选择对话框(类型由渲染层 Dialog 决定) */ OPEN_FILE_OR_FOLDER_DIALOG: 'agent:open-file-or-folder-dialog', /** 附加外部目录到 Agent 会话 */ ATTACH_DIRECTORY: 'agent:attach-directory',