Skip to content
Open
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
78 changes: 72 additions & 6 deletions apps/electron/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
AgentSaveFilesInput,
AgentSavedFile,
AgentAttachDirectoryInput,
AgentAttachFileInput,
GetTaskOutputInput,
GetTaskOutputResult,
StopTaskInput,
Expand Down Expand Up @@ -1008,6 +1009,24 @@ export function registerIpcHandlers(): void {
}
)

// 打开文件选择对话框(返回文件路径,用于附加)
ipcMain.handle(
AGENT_IPC_CHANNELS.OPEN_FILE_DIALOG,
async (): Promise<string[] | null> => {
const win = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0]
if (!win) return null

const result = await dialog.showOpenDialog(win, {
properties: ['openFile', 'multiSelections'],
title: '选择文件',
})

if (result.canceled || result.filePaths.length === 0) return null

return result.filePaths
}
)

// 打开文件夹选择对话框
ipcMain.handle(
AGENT_IPC_CHANNELS.OPEN_FOLDER_DIALOG,
Expand Down Expand Up @@ -1062,6 +1081,36 @@ export function registerIpcHandlers(): void {
}
)

// 附加外部文件到 Agent 会话
ipcMain.handle(
AGENT_IPC_CHANNELS.ATTACH_FILE,
async (_, input: AgentAttachFileInput): Promise<string[]> => {
const meta = getAgentSessionMeta(input.sessionId)
if (!meta) throw new Error(`会话不存在: ${input.sessionId}`)

const existing = meta.attachedFiles ?? []
if (existing.includes(input.filePath)) return existing

const updated = [...existing, input.filePath]
updateAgentSessionMeta(input.sessionId, { attachedFiles: updated })
return updated
}
)

// 移除会话的附加文件
ipcMain.handle(
AGENT_IPC_CHANNELS.DETACH_FILE,
async (_, input: AgentAttachFileInput): Promise<string[]> => {
const meta = getAgentSessionMeta(input.sessionId)
if (!meta) throw new Error(`会话不存在: ${input.sessionId}`)

const existing = meta.attachedFiles ?? []
const updated = existing.filter((f) => f !== input.filePath)
updateAgentSessionMeta(input.sessionId, { attachedFiles: updated })
return updated
}
)

// ===== Agent 文件系统操作 =====

// 获取 session 工作路径
Expand Down Expand Up @@ -1279,12 +1328,12 @@ export function registerIpcHandlers(): void {
}
)

// 搜索工作区文件(用于 @ 引用,递归扫描,支持附加目录)
// 搜索工作区文件(用于 @ 引用,递归扫描,支持附加目录/文件
ipcMain.handle(
AGENT_IPC_CHANNELS.SEARCH_WORKSPACE_FILES,
async (_, rootPath: string, query: string, limit = 20, additionalPaths?: string[]): Promise<FileSearchResult> => {
const { readdirSync } = await import('node:fs')
const { resolve, relative } = await import('node:path')
const { readdirSync, statSync } = await import('node:fs')
const { resolve, relative, basename } = await import('node:path')

const safeRoot = resolve(rootPath)
const ignoreDirs = new Set(['node_modules', '.git', 'dist', '.next', '__pycache__', '.venv', 'build', '.cache'])
Expand Down Expand Up @@ -1319,11 +1368,28 @@ export function registerIpcHandlers(): void {

scan(safeRoot, 0, safeRoot)

// 扫描附加目录(外部路径)
// 扫描附加目录/文件(外部路径)
if (additionalPaths && additionalPaths.length > 0) {
for (const addPath of additionalPaths) {
const addRoot = resolve(addPath)
scan(addRoot, 0, addRoot)
const resolvedPath = resolve(addPath)
try {
const stat = statSync(resolvedPath)
if (stat.isDirectory()) {
scan(resolvedPath, 0, resolvedPath)
continue
}

if (stat.isFile()) {
allEntries.push({
name: basename(resolvedPath),
// 单文件引用需传绝对路径,确保 SDK 可直接定位
path: resolvedPath,
type: 'file',
})
}
} catch {
// 忽略不存在或无权限的附加路径
}
}
}

Expand Down
10 changes: 8 additions & 2 deletions apps/electron/src/main/lib/agent-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ export class AgentOrchestrator {
* 通过 EventBus 分发 AgentEvent,通过 callbacks 发送控制信号。
*/
async sendMessage(input: AgentSendInput, callbacks: SessionCallbacks): Promise<void> {
const { sessionId, userMessage, channelId, modelId, workspaceId, additionalDirectories } = input
const { sessionId, userMessage, channelId, modelId, workspaceId, additionalDirectories, attachedFiles } = input
const stderrChunks: string[] = []

// 0. 并发保护
Expand Down Expand Up @@ -822,7 +822,13 @@ export class AgentOrchestrator {
resumeSessionId: existingSdkSessionId,
...(Object.keys(mcpServers).length > 0 && { mcpServers }),
...(workspaceSlug && { plugins: [{ type: 'local' as const, path: getAgentWorkspacePath(workspaceSlug) }] }),
...(additionalDirectories && additionalDirectories.length > 0 && { additionalDirectories }),
// 合并附加目录和附加文件到 additionalDirectories(SDK 支持文件路径)
...((additionalDirectories && additionalDirectories.length > 0) || (attachedFiles && attachedFiles.length > 0)) && {
additionalDirectories: [
...(additionalDirectories || []),
...(attachedFiles || []),
],
},
// SDK 0.2.52+ 新增选项(从 settings 读取)
...(appSettings.agentThinking && { thinking: appSettings.agentThinking }),
...(appSettings.agentEffort && { effort: appSettings.agentEffort }),
Expand Down
2 changes: 1 addition & 1 deletion apps/electron/src/main/lib/agent-session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export function appendAgentMessage(id: string, message: AgentMessage): void {
*/
export function updateAgentSessionMeta(
id: string,
updates: Partial<Pick<AgentSessionMeta, 'title' | 'channelId' | 'sdkSessionId' | 'workspaceId' | 'pinned' | 'attachedDirectories'>>,
updates: Partial<Pick<AgentSessionMeta, 'title' | 'channelId' | 'sdkSessionId' | 'workspaceId' | 'pinned' | 'attachedDirectories' | 'attachedFiles'>>,
): AgentSessionMeta {
const index = readIndex()
const idx = index.sessions.findIndex((s) => s.id === id)
Expand Down
22 changes: 22 additions & 0 deletions apps/electron/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type {
AgentSaveFilesInput,
AgentSavedFile,
AgentAttachDirectoryInput,
AgentAttachFileInput,
GetTaskOutputInput,
GetTaskOutputResult,
StopTaskInput,
Expand Down Expand Up @@ -402,6 +403,9 @@ export interface ElectronAPI {
/** 保存文件到 Agent session 工作目录 */
saveFilesToAgentSession: (input: AgentSaveFilesInput) => Promise<AgentSavedFile[]>

/** 打开文件选择对话框(返回文件路径,用于附加) */
openAgentFileDialog: () => Promise<string[] | null>

/** 打开文件夹选择对话框 */
openFolderDialog: () => Promise<{ path: string; name: string } | null>

Expand All @@ -411,6 +415,12 @@ export interface ElectronAPI {
/** 移除会话的附加目录 */
detachDirectory: (input: AgentAttachDirectoryInput) => Promise<string[]>

/** 附加外部文件到 Agent 会话 */
attachFile: (input: AgentAttachFileInput) => Promise<string[]>

/** 移除会话的附加文件 */
detachFile: (input: AgentAttachFileInput) => Promise<string[]>

// ===== Agent 文件系统操作 =====

/** 获取 session 工作路径 */
Expand Down Expand Up @@ -938,6 +948,10 @@ const electronAPI: ElectronAPI = {
return ipcRenderer.invoke(AGENT_IPC_CHANNELS.SAVE_FILES_TO_SESSION, input)
},

openAgentFileDialog: () => {
return ipcRenderer.invoke(AGENT_IPC_CHANNELS.OPEN_FILE_DIALOG)
},

openFolderDialog: () => {
return ipcRenderer.invoke(AGENT_IPC_CHANNELS.OPEN_FOLDER_DIALOG)
},
Expand All @@ -950,6 +964,14 @@ const electronAPI: ElectronAPI = {
return ipcRenderer.invoke(AGENT_IPC_CHANNELS.DETACH_DIRECTORY, input)
},

attachFile: (input: AgentAttachFileInput) => {
return ipcRenderer.invoke(AGENT_IPC_CHANNELS.ATTACH_FILE, input)
},

detachFile: (input: AgentAttachFileInput) => {
return ipcRenderer.invoke(AGENT_IPC_CHANNELS.DETACH_FILE, input)
},

// Agent 文件系统操作
getAgentSessionPath: (workspaceId: string, sessionId: string) => {
return ipcRenderer.invoke(AGENT_IPC_CHANNELS.GET_SESSION_PATH, workspaceId, sessionId)
Expand Down
7 changes: 7 additions & 0 deletions apps/electron/src/renderer/atoms/agent-atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,13 @@ export const agentSessionDraftsAtom = atom<Map<string, string>>(new Map())
*/
export const agentAttachedDirectoriesMapAtom = atom<Map<string, string[]>>(new Map())

/**
* 会话附加文件 Map — 以 sessionId 为 key
* 存储每个会话通过"选择文件"功能关联的外部文件路径列表。
* 这些文件通过引用方式附加,不复制到工作区。
*/
export const agentAttachedFilesMapAtom = atom<Map<string, string[]>>(new Map())

/** 当前 Agent 会话的草稿内容(派生读写原子) */
export const currentAgentSessionDraftAtom = atom(
(get) => {
Expand Down
25 changes: 23 additions & 2 deletions apps/electron/src/renderer/components/agent/AgentView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
buildTeamActivityEntries,
rebuildTeamDataFromMessages,
agentAttachedDirectoriesMapAtom,
agentAttachedFilesMapAtom,
} from '@/atoms/agent-atoms'
import type { AgentContextStatus } from '@/atoms/agent-atoms'
import { activeViewAtom } from '@/atoms/active-view'
Expand Down Expand Up @@ -91,6 +92,9 @@ export function AgentView({ sessionId }: { sessionId: string }): React.ReactElem
const setAttachedDirsMap = useSetAtom(agentAttachedDirectoriesMapAtom)
const attachedDirsMap = useAtomValue(agentAttachedDirectoriesMapAtom)
const attachedDirs = attachedDirsMap.get(sessionId) ?? []
const setAttachedFilesMap = useSetAtom(agentAttachedFilesMapAtom)
const attachedFilesMap = useAtomValue(agentAttachedFilesMapAtom)
const attachedFiles = attachedFilesMap.get(sessionId) ?? []

const draftsMap = useAtomValue(agentSessionDraftsAtom)
const setDraftsMap = useSetAtom(agentSessionDraftsAtom)
Expand Down Expand Up @@ -200,11 +204,13 @@ export function AgentView({ sessionId }: { sessionId: string }): React.ReactElem
.catch(console.error)
}, [sessionId, refreshVersion, setStreamingStates, store])

// 从会话元数据初始化附加目录
// 从会话元数据初始化附加目录和文件
const sessions = useAtomValue(agentSessionsAtom)
React.useEffect(() => {
const meta = sessions.find((s) => s.id === sessionId)
const dirs = meta?.attachedDirectories ?? []
const files = meta?.attachedFiles ?? []

setAttachedDirsMap((prev) => {
const existing = prev.get(sessionId)
// 避免不必要的更新
Expand All @@ -217,7 +223,20 @@ export function AgentView({ sessionId }: { sessionId: string }): React.ReactElem
}
return map
})
}, [sessionId, sessions, setAttachedDirsMap])

setAttachedFilesMap((prev) => {
const existing = prev.get(sessionId)
// 避免不必要的更新
if (JSON.stringify(existing) === JSON.stringify(files)) return prev
const map = new Map(prev)
if (files.length > 0) {
map.set(sessionId, files)
} else {
map.delete(sessionId)
}
return map
})
}, [sessionId, sessions, setAttachedDirsMap, setAttachedFilesMap])

// 自动发送 pending prompt(从设置页"对话完成配置"触发)
React.useEffect(() => {
Expand Down Expand Up @@ -580,6 +599,7 @@ export function AgentView({ sessionId }: { sessionId: string }): React.ReactElem
modelId: agentModelId || undefined,
workspaceId: currentWorkspaceId || undefined,
...(attachedDirs.length > 0 && { additionalDirectories: attachedDirs }),
...(attachedFiles.length > 0 && { attachedFiles }),
}

setInputContent('')
Expand Down Expand Up @@ -854,6 +874,7 @@ export function AgentView({ sessionId }: { sessionId: string }): React.ReactElem
collapsible
workspacePath={sessionPath}
attachedDirs={attachedDirs}
attachedFiles={attachedFiles}
/>

{/* Footer 工具栏 */}
Expand Down
Loading