diff --git a/packages/host/directory-picker-browse/src/client/DirectoryBrowser.module.css b/packages/host/directory-picker-browse/src/client/DirectoryBrowser.module.css
index 6cce6fabd0..8c8033d825 100644
--- a/packages/host/directory-picker-browse/src/client/DirectoryBrowser.module.css
+++ b/packages/host/directory-picker-browse/src/client/DirectoryBrowser.module.css
@@ -151,6 +151,13 @@
scrollbar-width: none;
}
+/* Engines that predate scrollbar-width take the pseudo-element path (the
+ * two are mutually exclusive by construction — see ui-theme's scrollbar
+ * contract); hide the row's horizontal bar there too. */
+.millerRow::-webkit-scrollbar {
+ display: none;
+}
+
/* Columns split the row evenly around the divider (a solo column takes the
* whole row); 256px is the floor below which the row scrolls (scrollbar
* hidden, the effect pins the child pane into view) instead of squeezing
@@ -292,6 +299,13 @@
color: var(--dsw-alias-label-primary);
}
+/* Trailing pressed check (Menu's .check parallel): flex-none so wrap or
+ * narrow-viewport clamp pressure never squashes the glyph — the nowrap
+ * label refuses to shrink, leaving the icon as the only compressible item. */
+.toggleCheck {
+ flex: none;
+}
+
.footerGap {
flex: 1 1 0;
}
diff --git a/packages/host/directory-picker-browse/src/client/DirectoryBrowser.tsx b/packages/host/directory-picker-browse/src/client/DirectoryBrowser.tsx
index 4c67d74722..8507681150 100644
--- a/packages/host/directory-picker-browse/src/client/DirectoryBrowser.tsx
+++ b/packages/host/directory-picker-browse/src/client/DirectoryBrowser.tsx
@@ -73,30 +73,46 @@ function foldPathFor(sep: '\\' | '/'): (value: string) => string {
}
/**
- * Drops every trailing separator (`HOME=/home/u//` ships verbatim while
- * resolve() strips them) down to, but never past, one leading character —
- * which keeps the POSIX root `/` intact. A backslash drive root (`C:\`)
- * does lose its separator; that stays safe only because every comparison
- * trims both sides symmetrically.
+ * Lexically normalizes an absolute host path for comparisons: collapses
+ * repeated and trailing separators, drops `.` segments, and applies `..` —
+ * mirroring the backend's resolve() for the shapes an environment-supplied
+ * HOME legally carries verbatim (`/home/u/`, `/home//u`, `/home/u/.`)
+ * while the backend's paths arrive already resolved. A lexical mirror
+ * only: symlinks are the backend's business, and the input always
+ * contains the separator (it is an absolute path).
*/
-function trimTrailingSeparator(path: string, sep: '\\' | '/'): string {
- let end = path.length
- while (end > sep.length && path.endsWith(sep, end)) end -= sep.length
- return path.slice(0, end)
+function normalizePathFor(sep: '\\' | '/'): (value: string) => string {
+ return (value) => {
+ const segments = value.split(sep)
+ const head = segments.shift()
+ /* v8 ignore next -- narrowing guard: split always yields at least one segment. */
+ if (head === undefined) return value
+ const out: string[] = []
+ for (const segment of segments) {
+ if (segment === '' || segment === '.') continue
+ if (segment === '..') {
+ out.pop()
+ continue
+ }
+ out.push(segment)
+ }
+ return `${head}${sep}${out.join(sep)}`
+ }
}
/**
* Breadcrumb rows for display: inside the home subtree the chain starts at a
* localized Home crumb; outside it the full ancestry shows, the root labeled
- * by its own path. The home comparison folds per platform and normalizes a
- * trailing separator, so a typed-case Windows path or a `HOME=/home/u/`
+ * by its own path. The home comparison folds per platform and lexically
+ * normalizes both sides, so a typed-case Windows path or a `HOME=/home/u/.`
* shape still collapses to the Home crumb.
*/
function displayCrumbs(listing: DirectoryListing, homeLabel: string): DirectoryEntry[] {
const sep = separatorOf(listing)
const fold = foldPathFor(sep)
- const home = fold(trimTrailingSeparator(listing.home, sep))
- const homeIndex = listing.crumbs.findIndex(crumb => fold(trimTrailingSeparator(crumb.path, sep)) === home)
+ const normalize = normalizePathFor(sep)
+ const home = fold(normalize(listing.home))
+ const homeIndex = listing.crumbs.findIndex(crumb => fold(normalize(crumb.path)) === home)
if (homeIndex === -1) return listing.crumbs
const tail = listing.crumbs.slice(homeIndex + 1)
return [{ name: homeLabel, path: listing.home, hidden: false }, ...tail]
@@ -800,7 +816,7 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
onClick={() => { setShowHidden(prev => !prev) }}
>
{t('browser.showHidden')}
- {showHidden && }
+ {showHidden && }
diff --git a/packages/host/directory-picker-browse/tests/directory-browser.spec.tsx b/packages/host/directory-picker-browse/tests/directory-browser.spec.tsx
index c27522a6cf..e2adc75881 100644
--- a/packages/host/directory-picker-browse/tests/directory-browser.spec.tsx
+++ b/packages/host/directory-picker-browse/tests/directory-browser.spec.tsx
@@ -349,6 +349,16 @@ describe('DirectoryBrowser', () => {
expect(document.activeElement).toBe(screen.getByRole('button', { name: 'browser.editPath' }))
})
+ it('a home carrying dot segments is still the display root', async () => {
+ // os.homedir() ships HOME verbatim; the backend resolves listing paths.
+ const listDirectory = vi.fn(async (path?: string) => ({ ...listingFor(path), home: `${HOME}/foo/../.` }))
+ mount({ listDirectory })
+ await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
+ expect(columns()).toHaveLength(1)
+ expect(screen.getByRole('button', { name: 'browser.home' })).toBeTruthy()
+ expect(listDirectory).toHaveBeenCalledTimes(1)
+ })
+
it('a navigation to the filesystem root keeps the single wide level', async () => {
mount()
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
@@ -1176,6 +1186,7 @@ describe('DirectoryBrowser', () => {
expect(cancels.map(button => button.disabled).sort()).toEqual([false, true])
expect(screen.getByRole('button', { name: 'browser.open' }).disabled).toBe(true)
expect(screen.getByRole('button', { name: 'browser.editPath' }).disabled).toBe(true)
+ expect(screen.getByRole('button', { name: 'browser.showHidden' }).disabled).toBe(true)
for (const row of screen.getAllByRole('listitem')) {
expect(rowButton(row).disabled).toBe(true)
}