Add an opt-in project setting that lets the workspace file browser use slash-prefixed absolute paths while preserving existing confined defaults. - Add the allowAbsoluteFileBrowserPaths project setting, docs, and changeset release note. - Thread absolute-path validation through workspace file-browser listing, editing, file operations, and downloads without widening other path APIs. - Add Settings UI controls and keep settings-specific file pickers project-relative. - Cover absolute-path browsing, percent-literal paths, root navigation, and settings picker behavior with tests. Files changed: .changeset/fn-7264-absolute-file-browser-paths.md | 7 + docs/settings-reference.md | 3 + .../core/src/__tests__/settings-parity.test.ts | 8 + packages/core/src/settings-schema.ts | 5 + packages/core/src/types.ts | 5 + .../app/__tests__/settings-sections.test.tsx | 41 +++++ packages/dashboard/app/components/FileBrowser.tsx | 26 ++- .../dashboard/app/components/FileBrowserModal.tsx | 7 + .../dashboard/app/components/SettingsModal.tsx | 34 +++- .../components/__tests__/FileBrowserModal.test.tsx | 18 ++ .../__tests__/SettingsModal.general.test.tsx | 17 ++ .../settings/sections/GeneralSection.tsx | 9 + .../__tests__/useWorkspaceFileBrowser.test.ts | 18 ++ .../dashboard/app/hooks/useWorkspaceFileBrowser.ts | 20 ++- .../dashboard/src/__tests__/file-service.test.ts | 199 ++++++++++++++++++++- packages/dashboard/src/file-service.ts | 129 ++++++++----- 16 files changed, 489 insertions(+), 57 deletions(-) Fusion-Task-Id: FN-7264 Fusion-Task-Lineage: ec71facd-0345-4490-a3df-244b98e24c2d Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
119 lines
3.3 KiB
TypeScript
119 lines
3.3 KiB
TypeScript
import { useState, useEffect, useCallback } from "react";
|
|
import { getErrorMessage } from "@fusion/core";
|
|
import type { FileNode, FileListResponse } from "../api";
|
|
import { fetchWorkspaceFileList } from "../api";
|
|
|
|
interface UseWorkspaceFileBrowserReturn {
|
|
entries: FileNode[];
|
|
currentPath: string;
|
|
setPath: (path: string) => void;
|
|
loading: boolean;
|
|
error: string | null;
|
|
refresh: () => void;
|
|
}
|
|
|
|
interface UseWorkspaceFileBrowserOptions {
|
|
allowAbsolutePaths?: boolean;
|
|
}
|
|
|
|
function isSlashPrefixedAbsolutePath(path: string): boolean {
|
|
return path.startsWith("/");
|
|
}
|
|
|
|
/**
|
|
* Hook for browsing files in a selected workspace.
|
|
*
|
|
* @param workspace - The workspace identifier ("project" or task ID)
|
|
* @param enabled - Whether fetching is enabled
|
|
* @param projectId - Optional project ID for multi-project scoping
|
|
*/
|
|
export function useWorkspaceFileBrowser(
|
|
workspace: string,
|
|
enabled: boolean,
|
|
projectId?: string,
|
|
options: UseWorkspaceFileBrowserOptions = {},
|
|
): UseWorkspaceFileBrowserReturn {
|
|
const [entries, setEntries] = useState<FileNode[]>([]);
|
|
const [currentPath, setCurrentPath] = useState<string>(".");
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
const allowAbsolutePaths = options.allowAbsolutePaths !== false;
|
|
|
|
const refresh = useCallback(() => {
|
|
setRefreshKey((key) => key + 1);
|
|
}, []);
|
|
|
|
const setPath = useCallback((path: string) => {
|
|
if (!allowAbsolutePaths && isSlashPrefixedAbsolutePath(path)) {
|
|
setError("This picker only accepts project-relative paths");
|
|
return;
|
|
}
|
|
|
|
setCurrentPath(path);
|
|
setError(null);
|
|
}, [allowAbsolutePaths]);
|
|
|
|
/*
|
|
FNXC:FileBrowser 2026-06-29-19:35:
|
|
Workspace file pickers must start each workspace at root so SettingsModal directory/file pickers do not inherit editor selection state. FileBrowserModal restores its selected file path at the modal layer when it needs editor persistence across worktree switches.
|
|
|
|
FNXC:FileBrowserAbsolutePaths 2026-06-29-00:00:
|
|
Settings-modal path pickers save project-relative contracts for overlap ignore paths and worktree copy files. Keep absolute browsing opt-in at the top-level file browser by letting callers reject slash-prefixed navigation before it reaches settings form state.
|
|
*/
|
|
useEffect(() => {
|
|
setCurrentPath(".");
|
|
setError(null);
|
|
setEntries([]);
|
|
}, [workspace]);
|
|
|
|
useEffect(() => {
|
|
if (!enabled || !workspace) {
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
|
|
async function loadFiles() {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response: FileListResponse = await fetchWorkspaceFileList(
|
|
workspace,
|
|
currentPath === "." ? undefined : currentPath,
|
|
projectId,
|
|
);
|
|
|
|
if (!cancelled) {
|
|
setEntries(response.entries);
|
|
}
|
|
} catch (err) {
|
|
if (!cancelled) {
|
|
setError(getErrorMessage(err) || "Failed to load files");
|
|
setEntries([]);
|
|
}
|
|
} finally {
|
|
if (!cancelled) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void loadFiles();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [workspace, currentPath, enabled, refreshKey, projectId]);
|
|
|
|
return {
|
|
entries,
|
|
currentPath,
|
|
setPath,
|
|
loading,
|
|
error,
|
|
refresh,
|
|
};
|
|
}
|