feat(files): let the path picker jump to a typed path and sort by name or date

The picker's current-folder line was a read-only breadcrumb, so reaching a
deep folder meant tapping through every level, and the listing was fixed to
name order, so the file an agent had just written was somewhere in a
500-entry list.

The current folder is now an editable field: Enter or Go jumps there, a full
file path lands in its folder with that file selected, and a path that does
not resolve keeps the listing you had and says so, instead of the reset to
the root that a stale initialPath gets. A Sort control orders the listing by
name or modified time in either direction, folders always first, and the
choice is remembered per device like the hidden toggle. Each entry shows a
compact modified time (time of day today, month-day this year, else the
date).

GET /api/filesystem/browse stamps every entry with mtimeMs to make that
possible; the stat that already fetched a file's size now serves both, so
it is still one stat per entry. Entries without an mtime (an older server,
the in-container listing) sort after dated ones and then by name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McLWqCWBuQYGuPMScb4Aou
This commit is contained in:
shenlvkang-collab
2026-09-10 12:42:33 +08:00
co-authored by Claude Fable 5.1
parent 5b667264b4
commit 58b4cb06d8
8 changed files with 627 additions and 29 deletions
+10 -3
View File
@@ -876,6 +876,7 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
let type: FilesystemBrowseEntry['type'];
let size: number | undefined;
let mtimeMs: number | undefined;
const symlink = entry.isSymbolicLink();
if (entry.isDirectory()) {
type = 'directory';
@@ -886,6 +887,7 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
const targetStat = await fs.stat(targetPath);
type = targetStat.isDirectory() ? 'directory' : 'file';
if (type === 'file') size = targetStat.size;
mtimeMs = targetStat.mtimeMs;
} catch {
continue;
}
@@ -894,11 +896,15 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
}
if (isBlockedPickerPath(targetPath, blockedTrees, type === 'directory')) continue;
if (type === 'file' && size === undefined) {
if (mtimeMs === undefined) {
// One stat per entry: the modified time lets the picker sort by date, and
// a file's size rides along on the same call.
try {
size = (await fs.stat(targetPath)).size;
const targetStat = await fs.stat(targetPath);
mtimeMs = targetStat.mtimeMs;
if (type === 'file') size = targetStat.size;
} catch {
// The path is still selectable even when a size lookup races a change.
// The path is still selectable even when a stat races a change.
}
}
entries.push({
@@ -906,6 +912,7 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
path: visiblePath,
type,
size,
mtimeMs,
symlink: symlink || undefined,
previewKind: type === 'file' ? getFilesystemPreviewKind(entry.name) : undefined,
});