Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/electron/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2950,11 +2950,11 @@ export function registerIpcHandlers(): void {
}
)

// 打开支持文件与文件夹混合选择的 Composer 对话框
// 打开文件或文件夹选择对话框(类型由渲染层 Dialog 决定)
ipcMain.handle(
AGENT_IPC_CHANNELS.OPEN_FILE_OR_FOLDER_DIALOG,
async (): Promise<FileOrFolderDialogResult> => {
return openFileOrFolderDialog()
async (_, type: 'file' | 'folder'): Promise<FileOrFolderDialogResult> => {
return openFileOrFolderDialog(type)
}
)

Expand Down
41 changes: 13 additions & 28 deletions apps/electron/src/main/lib/attachment-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,37 +340,22 @@ export async function openFileDialog(): Promise<FileDialogResult> {
}

/**
* 打开 Agent Composer 的混合选择对话框。
* 文件按附件语义读取,文件夹只返回路径,交由会话目录授权流程处理。
* 打开 Agent Composer 的内容选择对话框。
* 类型选择由渲染层用 Proma 自身 Dialog 完成(见 AttachmentContentDialog),
* 此处按调用方指定的类型直接打开对应系统选择器:
* - 'file':文件选择器,文件按附件语义读取
* - 'folder':文件夹选择器,目录只返回路径,交由会话目录授权流程处理
*/
export async function openFileOrFolderDialog(): Promise<FileOrFolderDialogResult> {
export async function openFileOrFolderDialog(type: 'file' | 'folder'): Promise<FileOrFolderDialogResult> {
// 运行时守卫:非法类型直接拒绝,避免被静默当作 '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)
Expand All @@ -382,6 +367,6 @@ export async function openFileOrFolderDialog(): Promise<FileOrFolderDialogResult

const directories: FileDialogDirectory[] = []
const fileResult = readDialogFiles(result.filePaths, directories)
console.log(`[附件服务] 混合对话框选择了 ${fileResult.files.length} 个内存附件,${fileResult.largeFiles?.length ?? 0} 个大文件引用,${directories.length} 个目录,${fileResult.skippedFiles?.length ?? 0} 个跳过`)
console.log(`[附件服务] ${type === 'file' ? '文件' : '文件夹'}对话框选择了 ${fileResult.files.length} 个内存附件,${fileResult.largeFiles?.length ?? 0} 个大文件引用,${directories.length} 个目录,${fileResult.skippedFiles?.length ?? 0} 个跳过`)
return { ...fileResult, directories }
}
8 changes: 4 additions & 4 deletions apps/electron/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,8 +750,8 @@ export interface ElectronAPI {
/** 打开文件夹选择对话框 */
openFolderDialog: () => Promise<{ path: string; name: string } | null>

/** 打开支持文件与文件夹混合选择的 Composer 对话框 */
openFileOrFolderDialog: () => Promise<FileOrFolderDialogResult>
/** 打开支持文件与文件夹混合选择的 Composer 对话框(类型由渲染层 Dialog 决定) */
openFileOrFolderDialog: (type: 'file' | 'folder') => Promise<FileOrFolderDialogResult>

/** 附加外部目录到 Agent 会话 */
attachDirectory: (input: AgentAttachDirectoryInput) => Promise<string[]>
Expand Down Expand Up @@ -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) => {
Expand Down
30 changes: 26 additions & 4 deletions apps/electron/src/renderer/components/agent/AgentView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -1706,10 +1707,23 @@ export function AgentView({ sessionId }: { sessionId: string }): React.ReactElem
}
}, [addLargeDialogFilesAsReferences, setPendingFiles])

/** 打开混合选择器:文件作为附件,文件夹仅授权给当前会话。 */
const handleAttachContent = React.useCallback(async (): Promise<void> => {
/** 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<void> => {
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
Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -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="附加文件或文件夹"
>
<Paperclip className="size-[17px]" />
Expand Down Expand Up @@ -3265,6 +3281,12 @@ export function AgentView({ sessionId }: { sessionId: string }): React.ReactElem
</div>
</AgentSessionProvider>

<AttachmentContentDialog
open={attachContentDialogOpen}
onOpenChange={setAttachContentDialogOpen}
onSelect={(type) => void handleAttachContentType(type)}
/>

<Dialog open={todoDialogOpen} onOpenChange={setTodoDialogOpen}>
<DialogContent className="max-w-lg">
<DialogHeader><DialogTitle>标记为 Todo</DialogTitle></DialogHeader>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>附加文件或文件夹</DialogTitle>
<DialogDescription>请选择要附加的内容类型</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
取消
</Button>
{/* 文件为默认操作:焦点(autoFocus)、Enter 默认触发、视觉主按钮三者统一 */}
<Button type="button" variant="default" autoFocus onClick={() => onSelect('file')}>
<FileText className="size-4" />
文件
</Button>
<Button type="button" variant="outline" onClick={() => onSelect('folder')}>
<Folder className="size-4" />
文件夹
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
2 changes: 1 addition & 1 deletion packages/shared/src/types/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down