From 0d71d8acb67e3612a5fee1bf33cfc948be64488f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Tue, 7 Apr 2026 16:28:08 +0800 Subject: [PATCH] feat: use native folder dialog in Tauri, keep directory tree for web DirectoryPicker now detects runtime environment: - Tauri desktop: opens native OS folder picker via @tauri-apps/plugin-dialog - Web browser: falls back to backend API directory tree browser Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/shared/DirectoryPicker.tsx | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/desktop/src/components/shared/DirectoryPicker.tsx b/desktop/src/components/shared/DirectoryPicker.tsx index 057937c1..cacfa273 100644 --- a/desktop/src/components/shared/DirectoryPicker.tsx +++ b/desktop/src/components/shared/DirectoryPicker.tsx @@ -9,8 +9,12 @@ type Props = { type DirEntry = { name: string; path: string; isDirectory: boolean } +function isTauriRuntime() { + return typeof window !== 'undefined' && ('__TAURI_INTERNALS__' in window || '__TAURI__' in window) +} + export function DirectoryPicker({ value, onChange }: Props) { - const [open, setOpen] = useState(false) + const [isOpen, setIsOpen] = useState(false) const [mode, setMode] = useState<'recent' | 'browse'>('recent') const [projects, setProjects] = useState([]) const [browseEntries, setBrowseEntries] = useState([]) @@ -21,23 +25,23 @@ export function DirectoryPicker({ value, onChange }: Props) { // Close on outside click useEffect(() => { - if (!open) return + if (!isOpen) return const handleClick = (e: MouseEvent) => { - if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false) + if (ref.current && !ref.current.contains(e.target as Node)) setIsOpen(false) } document.addEventListener('mousedown', handleClick) return () => document.removeEventListener('mousedown', handleClick) - }, [open]) + }, [isOpen]) // Load recent projects when opened useEffect(() => { - if (!open || mode !== 'recent') return + if (!isOpen || mode !== 'recent') return setLoading(true) sessionsApi.getRecentProjects() .then(({ projects }) => setProjects(projects)) .catch(() => setProjects([])) .finally(() => setLoading(false)) - }, [open, mode]) + }, [isOpen, mode]) const loadBrowseDir = async (path?: string) => { setLoading(true) @@ -52,13 +56,30 @@ export function DirectoryPicker({ value, onChange }: Props) { const handleSelect = (path: string) => { onChange(path) - setOpen(false) + setIsOpen(false) setMode('recent') } - const switchToBrowse = () => { - setMode('browse') - loadBrowseDir(value || undefined) + const handleChooseFolder = async () => { + if (isTauriRuntime()) { + // Desktop: native OS folder dialog + setIsOpen(false) + try { + const { open } = await import('@tauri-apps/plugin-dialog') + const selected = await open({ + directory: true, + multiple: false, + title: 'Choose project folder', + }) + if (selected) onChange(selected) + } catch (err) { + console.error('[DirectoryPicker] Failed to open folder dialog:', err) + } + } else { + // Web browser: directory tree via backend API + setMode('browse') + loadBrowseDir(value || undefined) + } } // Find selected project info @@ -69,10 +90,9 @@ export function DirectoryPicker({ value, onChange }: Props) { {/* Trigger — shows selected project chip or placeholder */} {value ? ( ) : (