feat: add browsable directory picker for new sessions and project switch
Users had to type an absolute server path on a phone keyboard to pick a working directory (#49 "Choose project UIX"), and #57 reports that only the default-drive project is ever discoverable. #52 already added recents + client.project.list() as flat pickers, but there was still no way to browse into subdirectories or discover paths the server hadn't already indexed as a "project" — the only fallback was manual typing. The opencode server already exposes a scoped filesystem-listing endpoint (GET /file, handled in file.ts/handlers/file.ts) that resolves relative to whatever directory the request is scoped to (header or query param) — no new server endpoint is needed. Add file.list() to the mobile SDK client and a new DirectoryBrowserSheet that lists subdirectories one level at a time (via clientForDirectory(dir) + file.list({path: "."})), supports "up" navigation, and a manual jump-to-path field. Wire it into both the "new session" modal and the existing DirectorySwitcher, so recents/manual entry remain available as a fallback alongside browsing. Residual gap: there's still no "list available drives" API, so Windows users with projects on D:, E:, etc. still need to type the drive root once (it's then remembered via recents) — a full fix for #57 would need a small server-side addition to enumerate mounted volumes.
This commit is contained in:
@@ -22,7 +22,7 @@ import { useConnections } from "../../src/stores/connections"
|
||||
import { useCatalog } from "../../src/stores/catalog"
|
||||
import type BottomSheet from "@gorhom/bottom-sheet"
|
||||
import type { Session, Project } from "../../src/lib/sdk"
|
||||
import { DirectorySwitcher } from "../../src/components/chat"
|
||||
import { DirectorySwitcher, DirectoryBrowserSheet } from "../../src/components/chat"
|
||||
|
||||
function formatTime(timestamp: number): string {
|
||||
const date = new Date(timestamp)
|
||||
@@ -129,6 +129,11 @@ export default function SessionsScreen() {
|
||||
} = useConnections()
|
||||
const loadCatalog = useCatalog((s) => s.load)
|
||||
const dirSheetRef = useRef<BottomSheet>(null)
|
||||
const browserSheetRef = useRef<BottomSheet>(null)
|
||||
const [browseStartDir, setBrowseStartDir] = useState<string | null>(null)
|
||||
// Shared folder browser is opened either to pick a directory for a new
|
||||
// session, or to switch the active connection's directory.
|
||||
const [browseMode, setBrowseMode] = useState<"create" | "switch">("create")
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
// Fetch server-known projects when the new session modal opens
|
||||
@@ -270,6 +275,27 @@ export default function SessionsScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
const openBrowser = useCallback(
|
||||
(startDir: string | null, mode: "create" | "switch") => {
|
||||
setBrowseStartDir(startDir || serverHome || null)
|
||||
setBrowseMode(mode)
|
||||
browserSheetRef.current?.expand()
|
||||
},
|
||||
[serverHome],
|
||||
)
|
||||
|
||||
const onBrowserSelect = useCallback(
|
||||
(directory: string) => {
|
||||
if (browseMode === "switch") {
|
||||
handleSwitchDirectory(directory)
|
||||
dirSheetRef.current?.close()
|
||||
} else {
|
||||
onCreateInDirectory(directory)
|
||||
}
|
||||
},
|
||||
[browseMode, handleSwitchDirectory, onCreateInDirectory],
|
||||
)
|
||||
|
||||
const onFabPress = () => {
|
||||
// Quick create in current project
|
||||
onCreateSession()
|
||||
@@ -478,6 +504,24 @@ export default function SessionsScreen() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Browse the server's filesystem instead of typing a path */}
|
||||
<TouchableOpacity
|
||||
style={[styles.projectRow, isDark && styles.projectRowDark, { marginTop: 16 }]}
|
||||
onPress={() =>
|
||||
openBrowser(currentProject?.path?.absolute || activeConnection?.directory || null, "create")
|
||||
}
|
||||
disabled={isCreating}
|
||||
>
|
||||
<Ionicons name="folder-open-outline" size={18} color={isDark ? "#8b5cf6" : "#6d28d9"} />
|
||||
<View style={styles.projectRowContent}>
|
||||
<Text style={[styles.projectRowName, isDark && styles.textDark]}>Browse Folders…</Text>
|
||||
<Text style={[styles.projectRowPath, isDark && styles.metaDark]}>
|
||||
Explore the server's filesystem
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={16} color={isDark ? "#666666" : "#999999"} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Manual path input fallback */}
|
||||
<Text style={[styles.modalLabel, isDark && styles.metaDark, { marginTop: 16 }]}>
|
||||
Enter Path Manually
|
||||
@@ -609,6 +653,19 @@ export default function SessionsScreen() {
|
||||
serverHome={serverHome}
|
||||
isDark={isDark}
|
||||
onSwitch={handleSwitchDirectory}
|
||||
onBrowse={() =>
|
||||
openBrowser(activeConnection?.directory || currentProject?.path?.absolute || null, "switch")
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Browsable folder picker — used for both "new session in..." and
|
||||
"switch project directory" flows (see browseMode). */}
|
||||
<DirectoryBrowserSheet
|
||||
sheetRef={browserSheetRef}
|
||||
startDirectory={browseStartDir}
|
||||
clientForDirectory={clientForDirectory}
|
||||
isDark={isDark}
|
||||
onSelect={onBrowserSelect}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user