feat: use native OS folder picker dialog in Electron mode

Electron 环境下改用系统原生文件夹选择对话框替代自定义 FolderPicker 组件,
浏览器环境保留原有 FolderPicker 作为 fallback(浏览器沙箱无法获取绝对路径)。

- electron/main.ts: 新增 `dialog:open-folder` IPC handler,调用
  `dialog.showOpenDialog` 并设置 openDirectory + createDirectory 属性,
  macOS 下支持在对话框内新建文件夹,对话框模态绑定 mainWindow
- electron/preload.ts: 在 electronAPI 中暴露 `dialog.openFolder()` 方法
- src/types/electron.d.ts: 新建全局类型声明,统一定义 window.electronAPI
  的完整类型,消除代码中散落的 `(window as any)` 类型断言
- src/hooks/useNativeFolderPicker.ts: 新建 hook 封装平台检测和原生对话框
  调用逻辑,返回 isElectron 标志和 openNativePicker 方法
- src/components/layout/ChatListPanel.tsx: 新增 openFolderPicker() 统一入口,
  Electron 走原生对话框、浏览器走 setFolderPickerOpen(true);替换
  handleNewChat 中 4 处及文件夹按钮 onClick 共 5 处调用点;
  handleFolderSelect 改为 useCallback 并调整声明顺序避免引用前使用;
  清理 electronAPI 类型断言
- src/app/chat/[id]/page.tsx: 移除 `window as unknown as ...` 类型断言
- src/components/layout/ConnectionStatus.tsx: 移除 `(window as any)` 断言
- src/components/layout/InstallWizard.tsx: getInstallAPI() 简化为直接返回
  window.electronAPI?.install,移除 10 行手动类型定义

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-26 10:47:08 +08:00
1 parent 5bace38e47
commit c6ed594800
8 files changed
+119 -43

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
/**
* Global type declarations for the Electron preload API.
* Exposed via contextBridge.exposeInMainWorld('electronAPI', ...) in electron/preload.ts.
*/
interface ElectronInstallAPI {
checkPrerequisites: () => Promise<{
hasNode: boolean;
nodeVersion?: string;
hasClaude: boolean;
claudeVersion?: string;
}>;
start: (options?: { includeNode?: boolean }) => Promise<void>;
cancel: () => Promise<void>;
getLogs: () => Promise<string[]>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onProgress: (callback: (data: any) => void) => () => void;
}
interface ElectronAPI {
versions: {
electron: string;
node: string;
chrome: string;
};
shell: {
openPath: (path: string) => Promise<string>;
};
dialog: {
openFolder: (options?: {
defaultPath?: string;
title?: string;
}) => Promise<{ canceled: boolean; filePaths: string[] }>;
};
install: ElectronInstallAPI;
}
declare global {
interface Window {
electronAPI?: ElectronAPI;
}
}
export {};