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:
engineer
2026-07-16 15:48:38 -07:00
parent a5723bf087
commit f0a7c1d868
5 changed files with 436 additions and 9 deletions

View File

@@ -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>
)