Skip to content

Commit 1034d22

Browse files
authored
feat(file): add GET /api/fs/browse shallow host-file picker (#235)
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. Co-authored-by: zk <>
1 parent d9be99f commit 1034d22

8 files changed

Lines changed: 571 additions & 14 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/aionui-api-types/src/file.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,64 @@ pub struct CancelZipRequest {
126126
pub request_id: String,
127127
}
128128

129+
/// Query parameters for `GET /api/fs/browse` — shallow directory browser.
130+
///
131+
/// Unlike `/api/fs/dir` (which returns a recursive tree scoped to a workspace
132+
/// root), `browse` is a WebUI-only host-file picker: it lists a single
133+
/// directory level, surfaces navigation hints (`can_go_up`, `parent_path`),
134+
/// and on Windows supports a `__ROOT__` sentinel for the drive-list screen.
135+
#[derive(Debug, Deserialize)]
136+
pub struct BrowseDirectoryQuery {
137+
/// Directory to list. Empty string means "use default" (Windows: drive
138+
/// list; Unix: current working directory). `"__ROOT__"` on Windows is
139+
/// treated the same as an empty path.
140+
#[serde(default)]
141+
pub path: Option<String>,
142+
/// When true, include regular files in the response. Defaults to false
143+
/// (directories only).
144+
#[serde(default)]
145+
pub show_files: Option<String>,
146+
}
147+
148+
/// A single entry in a `/api/fs/browse` response.
149+
///
150+
/// Uses camelCase on the wire to match the original Express contract the
151+
/// frontend `DirectorySelectionModal` still consumes.
152+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
153+
#[serde(rename_all = "camelCase")]
154+
pub struct BrowseEntry {
155+
pub name: String,
156+
pub path: String,
157+
pub is_directory: bool,
158+
pub is_file: bool,
159+
#[serde(skip_serializing_if = "Option::is_none")]
160+
pub size: Option<u64>,
161+
/// Last-modified time as milliseconds since the unix epoch. Absent when
162+
/// the entry has no readable metadata (e.g. a Windows drive stub).
163+
#[serde(skip_serializing_if = "Option::is_none")]
164+
pub modified: Option<i64>,
165+
}
166+
167+
/// Response body for `GET /api/fs/browse`.
168+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169+
#[serde(rename_all = "camelCase")]
170+
pub struct BrowseDirectoryResponse {
171+
/// The resolved directory currently being listed. Empty string when the
172+
/// response is a Windows drive-list screen.
173+
pub current_path: String,
174+
/// Path to navigate to when the user clicks "up". `None` when already at
175+
/// the root. Value `"__ROOT__"` is a sentinel used on Windows to mean
176+
/// "return to the drive-list screen".
177+
#[serde(skip_serializing_if = "Option::is_none")]
178+
pub parent_path: Option<String>,
179+
pub items: Vec<BrowseEntry>,
180+
pub can_go_up: bool,
181+
pub truncated: bool,
182+
/// True when the response represents the Windows drive-list screen.
183+
#[serde(skip_serializing_if = "Option::is_none")]
184+
pub is_root: Option<bool>,
185+
}
186+
129187
// ---------------------------------------------------------------------------
130188
// A. Core file operations — Response DTOs
131189
// ---------------------------------------------------------------------------

crates/aionui-api-types/src/lib.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,13 @@ pub use extension::{
7575
InstallExtensionRequest, PermissionDetailResponse, PermissionSummaryResponse,
7676
};
7777
pub use file::{
78-
CancelZipRequest, CopyFilesRequest, CopyFilesResponse, CreateTempFileRequest, DirOrFileResponse,
79-
FetchRemoteImageRequest, FileChangeInfoResponse, FileMetadataResponse, FileWatchRequest, GetFileMetadataRequest,
80-
GetFilesByDirRequest, GetImageBase64Request, ListWorkspaceFilesRequest, ReadFileBufferRequest, ReadFileRequest,
81-
RemoveEntryRequest, RenameRequest, RenameResponse, SnapshotBaselineRequest, SnapshotCompareResponse,
82-
SnapshotDiscardRequest, SnapshotInfoResponse, SnapshotMode, SnapshotStageRequest, SnapshotWorkspaceRequest,
83-
WorkspaceFlatFileResponse, WorkspaceOfficeWatchRequest, WriteFileRequest, ZipFileEntry, ZipRequest,
78+
BrowseDirectoryQuery, BrowseDirectoryResponse, BrowseEntry, CancelZipRequest, CopyFilesRequest, CopyFilesResponse,
79+
CreateTempFileRequest, DirOrFileResponse, FetchRemoteImageRequest, FileChangeInfoResponse, FileMetadataResponse,
80+
FileWatchRequest, GetFileMetadataRequest, GetFilesByDirRequest, GetImageBase64Request, ListWorkspaceFilesRequest,
81+
ReadFileBufferRequest, ReadFileRequest, RemoveEntryRequest, RenameRequest, RenameResponse, SnapshotBaselineRequest,
82+
SnapshotCompareResponse, SnapshotDiscardRequest, SnapshotInfoResponse, SnapshotMode, SnapshotStageRequest,
83+
SnapshotWorkspaceRequest, WorkspaceFlatFileResponse, WorkspaceOfficeWatchRequest, WriteFileRequest, ZipFileEntry,
84+
ZipRequest,
8485
};
8586
pub use lifecycle::{GitHubReleaseAsset, SystemInfoResponse, UpdateCheckRequest, UpdateCheckResult, UpdateReleaseInfo};
8687
pub use mcp::{

crates/aionui-app/src/state_builders.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ pub fn build_session_state(services: &AppServices, agent_service: Arc<AgentServi
223223
pub fn build_file_state(services: &AppServices) -> FileRouterState {
224224
let broadcaster = services.event_bus.clone();
225225
let allowed_roots = default_allowed_roots();
226+
let browse_roots = aionui_file::browse::default_browse_roots();
226227
let file_service = Arc::new(FileService::new(broadcaster.clone(), allowed_roots.clone()));
227228
let watch_service = Arc::new(FileWatchService::new(broadcaster).expect("file watch service initialization"));
228229
let snapshot_service = Arc::new(SnapshotService::new());
@@ -231,6 +232,7 @@ pub fn build_file_state(services: &AppServices) -> FileRouterState {
231232
watch_service,
232233
snapshot_service,
233234
allowed_roots,
235+
browse_roots,
234236
}
235237
}
236238

crates/aionui-file/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ serde.workspace = true
2323
serde_json.workspace = true
2424
tracing.workspace = true
2525
dashmap.workspace = true
26+
dirs.workspace = true
2627

2728
[dev-dependencies]
2829
tempfile = "3"

0 commit comments

Comments
 (0)