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:
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user