fix(directory-picker): address review — modal layering, root nav, stale state

- HIGH: the "Browse Folders..." entry in the New Session RN <Modal> expanded
  a sibling BottomSheet, which a native Modal always covers (a
  BottomSheetModal through the root portal would be covered too), so the
  primary entry point was invisible/untouchable. The modal is now closed
  before the sheet expands and restored on cancel via a new onDismiss
  callback (restoreNewSessionOnDismiss ref); picking a folder proceeds to
  session creation without reopening the modal.

- MEDIUM: parentOf("/") returned "/" so Up at the POSIX root looped forever;
  it now returns null at "/", "\" and Windows drive roots alike, disabling
  the Up button there.

- LOW: opening the sheet with no known start directory (server home not
  loaded yet) showed the previous open's stale entries; it now clears state,
  invalidates in-flight loads, and shows an "Enter a path above to start
  browsing" empty state. Sheet init also no longer re-runs on snap-point
  drags (wasOpen guard).

- Extracted the pure path helpers (stripTrailingSlash/parentOf/nameOf) into
  src/lib/path-utils.ts (no RN imports) with node --test coverage for POSIX
  root, Windows drive roots, trailing slashes, and backslash paths.

typecheck clean; 97/97 tests pass (16 new).
This commit is contained in:
engineer
2026-07-16 15:57:31 -07:00
parent f0a7c1d868
commit 7e9b3981c3
4 changed files with 184 additions and 30 deletions

View File

@@ -0,0 +1,88 @@
import { test } from "node:test"
import assert from "node:assert/strict"
import { stripTrailingSlash, parentOf, nameOf } from "./path-utils.ts"
// stripTrailingSlash -------------------------------------------------------
test("stripTrailingSlash: removes single and repeated trailing separators", () => {
assert.equal(stripTrailingSlash("/a/b/"), "/a/b")
assert.equal(stripTrailingSlash("/a/b///"), "/a/b")
assert.equal(stripTrailingSlash("C:\\proj\\"), "C:\\proj")
})
test("stripTrailingSlash: keeps input when stripping would empty it", () => {
assert.equal(stripTrailingSlash("/"), "/")
assert.equal(stripTrailingSlash("\\"), "\\")
assert.equal(stripTrailingSlash("///"), "///")
})
test("stripTrailingSlash: leaves paths without trailing separator unchanged", () => {
assert.equal(stripTrailingSlash("/a/b"), "/a/b")
assert.equal(stripTrailingSlash("D:/work"), "D:/work")
})
// parentOf -----------------------------------------------------------------
test("parentOf: POSIX root has no parent", () => {
assert.equal(parentOf("/"), null)
assert.equal(parentOf("//"), null)
})
test("parentOf: bare backslash has no parent", () => {
assert.equal(parentOf("\\"), null)
})
test("parentOf: Windows drive roots have no parent", () => {
assert.equal(parentOf("C:\\"), null)
assert.equal(parentOf("D:/"), null)
assert.equal(parentOf("c:"), null)
})
test("parentOf: first-level POSIX dir returns the root", () => {
assert.equal(parentOf("/home"), "/")
assert.equal(parentOf("/home/"), "/")
})
test("parentOf: nested POSIX paths walk up one level", () => {
assert.equal(parentOf("/home/user/project"), "/home/user")
assert.equal(parentOf("/home/user/project/"), "/home/user")
})
test("parentOf: first-level Windows dir returns the drive root", () => {
assert.equal(parentOf("C:\\projects"), "C:\\")
assert.equal(parentOf("D:/work"), "D:\\")
})
test("parentOf: nested Windows backslash paths walk up one level", () => {
assert.equal(parentOf("C:\\projects\\app"), "C:\\projects")
assert.equal(parentOf("C:\\projects\\app\\"), "C:\\projects")
})
test("parentOf: mixed-separator Windows paths walk up one level", () => {
assert.equal(parentOf("D:/work/repo"), "D:/work")
})
test("parentOf: relative segment without separators has no parent", () => {
assert.equal(parentOf("project"), null)
})
// nameOf ---------------------------------------------------------------------
test("nameOf: returns the last POSIX segment", () => {
assert.equal(nameOf("/home/user/project"), "project")
assert.equal(nameOf("/home/user/project/"), "project")
})
test("nameOf: returns the last Windows segment", () => {
assert.equal(nameOf("C:\\projects\\app"), "app")
assert.equal(nameOf("D:/work/repo"), "repo")
})
test("nameOf: root paths fall back to the trimmed input", () => {
assert.equal(nameOf("/"), "/")
assert.equal(nameOf("C:\\"), "C:")
})
test("nameOf: bare segment is returned as-is", () => {
assert.equal(nameOf("project"), "project")
})

38
src/lib/path-utils.ts Normal file
View File

@@ -0,0 +1,38 @@
// Pure absolute-path helpers for the server-filesystem browser.
// No React Native imports — unit-testable with node --test.
//
// Server working directories can be POSIX (/a/b) or Windows (C:\a\b, D:/a/b)
// since the mobile app can point at either kind of opencode server, so both
// separators are handled.
/** Remove trailing slashes/backslashes, keeping the input if that would empty it. */
export function stripTrailingSlash(dir: string): string {
return dir.replace(/[\\/]+$/, "") || dir
}
function isRoot(trimmed: string): boolean {
// POSIX root ("/", "//"), a bare backslash, or a Windows drive root ("C:").
return /^[\\/]+$/.test(trimmed) || /^[a-zA-Z]:$/.test(trimmed)
}
/**
* Parent directory of an absolute path, or null when already at a
* filesystem root (POSIX "/" or a Windows drive root like "C:\").
*/
export function parentOf(dir: string): string | null {
const trimmed = stripTrailingSlash(dir)
if (isRoot(trimmed)) return null
const lastSlash = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"))
if (lastSlash < 0) return null
const head = trimmed.slice(0, lastSlash)
if (!head) return trimmed[0] === "\\" ? "\\" : "/" // reached posix root
if (/^[a-zA-Z]:$/.test(head)) return `${head}\\` // reached a windows drive root
return head
}
/** Last path segment, e.g. "/a/b/" -> "b", "C:\\proj" -> "proj". */
export function nameOf(dir: string): string {
const trimmed = stripTrailingSlash(dir)
const lastSlash = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"))
return lastSlash >= 0 ? trimmed.slice(lastSlash + 1) || trimmed : trimmed
}