Improve folder picker path input (#372)

## Summary
- Adds editable path entry directly inside the folder browser dialog
while keeping browse-first behavior.
- Removes the multi-root workspace picker changes from the source
implementation.
- Refines responsive controls so mobile shows the path field first, then
New Folder and Open actions together.

## Credits
- Based on the work and request flow from #350. Thanks to the original
requester and contributor there for the folder picker path input idea.

## Verification
- npm run typecheck --workspace @neuralnomads/codenomad
- npm run typecheck --workspace @codenomad/ui

---------

Co-authored-by: Pascal André <pascalandr@gmail.com>
This commit is contained in:
Shantur Rathore
2026-04-26 14:31:01 +01:00
committed by GitHub
parent e17f346581
commit 2a25abce03
15 changed files with 1010 additions and 84 deletions

View File

@@ -141,9 +141,13 @@ export interface WorkspaceLogEntry {
export interface FileSystemEntry {
name: string
/** Path relative to the CLI server root ("." represents the root itself). */
/**
* Path identifier for the entry. Relative to the server root in restricted
* single-root listings ("." represents the root itself); absolute in
* unrestricted, drives, and multi-root top-level listings.
*/
path: string
/** Absolute path when available (unrestricted listings). */
/** Absolute path when available (unrestricted and multi-root listings). */
absolutePath?: string
type: "file" | "directory"
size?: number
@@ -156,7 +160,12 @@ export type FileSystemPathKind = "relative" | "absolute" | "drives"
export interface FileSystemListingMetadata {
scope: FileSystemScope
/** Canonical identifier of the current view ("." for restricted roots, absolute paths otherwise). */
/**
* Canonical identifier of the current view:
* - "." for restricted single-root listings
* - WINDOWS_DRIVES_ROOT for the Windows drives pseudo-root
* - absolute path otherwise
*/
currentPath: string
/** Optional parent path if navigation upward is allowed. */
parentPath?: string
@@ -166,7 +175,7 @@ export interface FileSystemListingMetadata {
homePath: string
/** Human-friendly label for the current path. */
displayPath: string
/** Indicates whether entry paths are relative, absolute, or represent drive roots. */
/** Indicates whether entry paths are relative, absolute, or represent the drive pseudo-view. */
pathKind: FileSystemPathKind
}
@@ -188,7 +197,7 @@ export interface FileSystemCreateFolderRequest {
export interface FileSystemCreateFolderResponse {
/**
* Path identifier that can be passed back to `/api/filesystem` to browse the new folder.
* Relative for restricted listings, absolute for unrestricted.
* Relative for restricted listings and absolute for unrestricted listings.
*/
path: string
/** Absolute folder path on the server host. */

View File

@@ -263,6 +263,19 @@ export class FileSystemBrowser {
if (!input || input === "." || input === "./" || input === "/") {
return "."
}
if (path.isAbsolute(input)) {
const resolved = path.resolve(input)
const relativeToRoot = path.relative(this.root, resolved)
if (relativeToRoot === "") {
return "."
}
if (this.isOutsideRoot(relativeToRoot)) {
throw new Error("Access outside of root is not allowed")
}
return relativeToRoot.replace(/\\+/g, "/")
}
let normalized = input.replace(/\\+/g, "/")
if (normalized.startsWith("./")) {
normalized = normalized.replace(/^\.\/+/, "")
@@ -293,12 +306,16 @@ export class FileSystemBrowser {
const normalized = this.normalizeRelativePath(relativePath)
const target = path.resolve(this.root, normalized)
const relativeToRoot = path.relative(this.root, target)
if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot) && relativeToRoot !== "") {
if (this.isOutsideRoot(relativeToRoot)) {
throw new Error("Access outside of root is not allowed")
}
return target
}
private isOutsideRoot(relativeToRoot: string) {
return relativeToRoot === ".." || relativeToRoot.startsWith(`..${path.sep}`) || path.isAbsolute(relativeToRoot)
}
private resolveUnrestrictedPath(input: string | undefined): string {
if (!input || input === "." || input === "./") {
return this.homeDir

View File

@@ -317,7 +317,10 @@ async function main() {
getServerBaseUrl: () => serverMeta.localUrl,
nodeExtraCaCertsPath,
})
const fileSystemBrowser = new FileSystemBrowser({ rootDir: options.rootDir, unrestricted: options.unrestrictedRoot })
const fileSystemBrowser = new FileSystemBrowser({
rootDir: options.rootDir,
unrestricted: options.unrestrictedRoot,
})
const instanceStore = new InstanceStore(configLocation.instancesDir)
const speechService = new SpeechService(settings, logger.child({ component: "speech" }))
const sidecarManager = new SideCarManager({