Skip to content

feat(file): add GET /api/fs/browse shallow host-file picker#235

Merged
kaizhou-lab merged 1 commit into
mainfrom
feat/fs-browse
May 12, 2026
Merged

feat(file): add GET /api/fs/browse shallow host-file picker#235
kaizhou-lab merged 1 commit into
mainfrom
feat/fs-browse

Conversation

@kaizhou-lab

Copy link
Copy Markdown
Contributor

Summary

  • Restores the WebUI host-file browsing endpoint that was removed in M6 when the legacy Express `directoryApi.ts` was deleted but its frontend caller (`DirectorySelectionModal`) was left untouched, causing 404s on WebUI "Host Files" selection.
  • Ports the pre-M6 behavior to Rust as `GET /api/fs/browse`: shallow single-level listing with navigation hints (`canGoUp`, `parentPath`), hidden-file filtering, directories-first sort, 500-item truncation, and a `ROOT` sentinel for the Windows drive-picker screen.
  • Widens the allowed-roots policy for this endpoint only — `cwd` + `home` + (Windows) drive letters + (Unix) `/` — because the host-file picker is not workspace-scoped. Wired through `FileRouterState.browse_roots` so the allow-list is injectable for tests.

Why a separate route instead of reusing `/api/fs/dir`?

`/api/fs/dir` returns a recursive tree scoped to a workspace root and requires a root param. The host-file picker is fundamentally different: it walks one level at a time, needs parent/up-hints for navigation, and must reach outside any workspace (including drive letters on Windows). Overloading the existing route would weaken its invariants; a new route keeps workspace semantics intact.

Frontend PR

to be linked — will point to the AionUi repo PR that flips `DirectorySelectionModal` from `/api/directory/browse` to `/api/fs/browse`.

Test plan

  • 13 unit tests in `crates/aionui-file/src/browse.rs` covering happy path, show_files toggle, hidden-file filtering, traversal rejection (forbidden / not-found / bad-request), symlink escape rejection, can_go_up sandbox boundary, and truncation
  • `cargo check --workspace` green
  • `cargo test -p aionui-file` — all 328 tests pass
  • Manual smoke in WebUI (via the frontend PR) — reviewer's call whether to do before merge

Restores the WebUI host-file browsing endpoint removed during M6 by
porting the pre-M6 Express directoryApi.ts to Rust. Unlike /api/fs/dir
(workspace tree) this is a shallow lister with navigation hints
(can_go_up, parent_path) and a __ROOT__ sentinel for the Windows drive
picker. Allowed roots widen to cwd + home + Windows drives + "/" on
Unix because the host-file picker legitimately needs to reach outside
any single workspace. Wired through FileRouterState.browse_roots so
the allow-list can be injected in tests.

Unblocks the DirectorySelectionModal in WebUI mode which was hitting
404s against the legacy URL.
@kaizhou-lab kaizhou-lab enabled auto-merge (squash) May 12, 2026 02:01
@kaizhou-lab kaizhou-lab disabled auto-merge May 12, 2026 02:01
@kaizhou-lab kaizhou-lab merged commit 1034d22 into main May 12, 2026
1 of 5 checks passed
@kaizhou-lab kaizhou-lab deleted the feat/fs-browse branch May 12, 2026 02:02
piorpua pushed a commit that referenced this pull request May 12, 2026
Collapse nested if-let into a single condition with `&&` to
satisfy clippy::collapsible_if (introduced in #235).
zhuqingyv pushed a commit that referenced this pull request May 12, 2026
Collapse nested if-let into a single condition with `&&` to
satisfy clippy::collapsible_if (introduced in #235).
piorpua pushed a commit that referenced this pull request Jun 11, 2026
)

## Problem

iOfficeAI/AionUi#3191 — on Windows, selecting a directory in the WebUI
picker stores paths like `\\?\C:\DEV\xxxxx` as the conversation
workspace. Claude Code then fails to connect (cmd.exe-based CLI shims
reject verbatim working directories), and the same directory shows up as
two different projects in the sidebar (`C:\DEV\xxxxx` from old sessions
vs `\\?\C:\DEV\xxxxx` from new ones).

## Root cause

The `/api/fs/browse` endpoint (added in #235, replacing the legacy
Express implementation in AionUi) resolves paths with
`fs::canonicalize`, which on Windows returns extended-length (verbatim)
paths — `\\?\C:\DEV`. Those strings were serialized verbatim into
`current_path`, `parent_path` and every item `path`. The legacy Express
endpoint built paths with Node's `path.join` and never produced this
form, so this is an M6-cutover regression.

## Fix

Strip `\\?\` (and `\\?\UNC\` → `\\`) at the response-serialization
boundary in `browse.rs`. Internal sandbox checks keep comparing
canonical (verbatim) forms, so the allow-list semantics are unchanged.

## Tests

- Cross-platform string-level unit tests for the strip helper (covered
by Linux CI)
- `#[cfg(windows)]` end-to-end regression test asserting no response
path carries the verbatim prefix (fails on Windows without this fix)
- `cargo test` full workspace: 6022 passed; clippy `-D warnings` + fmt
clean

Frontend defensive counterpart: iOfficeAI/AionUi PR (renderer-side
normalization for older backend binaries) follows.

Fixes iOfficeAI/AionUi#3191
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant