FileTree can reveal a path

For a tree whose selection is driven from outside — a nav beside it, a URL — where a
folder left shut around the open file hides the one thing being looked at. Every directory
on the way opens, loading as it goes.

It only ever opens. A node that closed itself when the selection moved elsewhere would
collapse the tree under somebody as they clicked around it.
This commit is contained in:
2026-09-06 15:25:13 +01:00
parent fb5304563b
commit 5fa3f3ec76
@@ -21,6 +21,14 @@ type FileTreeProps = {
* an agent had just written to showing the listing from before its turn. * an agent had just written to showing the listing from before its turn.
*/ */
refreshToken?: number; refreshToken?: number;
/**
* A file to reveal: every directory on the way to it opens, loading as it goes.
*
* For a tree whose selection is driven from OUTSIDE — a nav beside it, a URL — where a folder left
* shut around the open file hides the one thing being looked at. Expanding is still disclosure and
* still remembered, so anything opened by hand stays open.
*/
revealPath?: string | null;
}; };
type TreeNodeProps = { type TreeNodeProps = {
@@ -31,6 +39,7 @@ type TreeNodeProps = {
searchForFile?: ((path: string) => string) | null; searchForFile?: ((path: string) => string) | null;
depth: number; depth: number;
refreshToken?: number; refreshToken?: number;
revealPath?: string | null;
}; };
const FileIcon = ({ name }: { name: string }) => { const FileIcon = ({ name }: { name: string }) => {
@@ -44,7 +53,16 @@ const sortEntries = (entries: DirEntry[]) => {
return [...dirs, ...files]; return [...dirs, ...files];
}; };
const TreeNode = ({ entry, parentPath, root, onOpenFile, searchForFile, depth, refreshToken }: TreeNodeProps) => { const TreeNode = ({
entry,
parentPath,
root,
onOpenFile,
searchForFile,
depth,
refreshToken,
revealPath,
}: TreeNodeProps) => {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const [children, setChildren] = useState<DirEntry[] | null>(null); const [children, setChildren] = useState<DirEntry[] | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -73,6 +91,32 @@ const TreeNode = ({ entry, parentPath, root, onOpenFile, searchForFile, depth, r
}; };
}, [refreshToken]); }, [refreshToken]);
/*
On the way to the revealed file: open, and load if this is the first time.
Only ever opens. A node that closes itself when the selection moves elsewhere would collapse the
tree under somebody as they clicked around it.
*/
const onTheWay = !!revealPath && isDir && revealPath.startsWith(`${fullPath}/`);
useEffect(() => {
if (!onTheWay) return;
setExpanded(true);
if (children !== null) return;
let alive = true;
listDirRef
.current(fullPath)
.then((res) => alive && setChildren(sortEntries(res.entries)))
.catch(() => alive && setChildren([]));
return () => {
alive = false;
};
// `children` is deliberately absent: this must run when the PATH changes, not when the load it
// started completes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onTheWay, fullPath]);
const handleClick = async () => { const handleClick = async () => {
if (!isDir) { if (!isDir) {
onOpenFile(fullPath, entry.name); onOpenFile(fullPath, entry.name);
@@ -140,6 +184,7 @@ const TreeNode = ({ entry, parentPath, root, onOpenFile, searchForFile, depth, r
searchForFile={searchForFile} searchForFile={searchForFile}
depth={depth + 1} depth={depth + 1}
refreshToken={refreshToken} refreshToken={refreshToken}
revealPath={revealPath}
/> />
))} ))}
</div> </div>
@@ -148,7 +193,7 @@ const TreeNode = ({ entry, parentPath, root, onOpenFile, searchForFile, depth, r
); );
}; };
export const FileTree = ({ root, basePath, onOpenFile, searchForFile, refreshToken }: FileTreeProps) => { export const FileTree = ({ root, basePath, onOpenFile, searchForFile, refreshToken, revealPath }: FileTreeProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { listDir } = useFilesAPI(root); const { listDir } = useFilesAPI(root);
const [entries, setEntries] = useState<DirEntry[] | null>(null); const [entries, setEntries] = useState<DirEntry[] | null>(null);
@@ -208,6 +253,7 @@ export const FileTree = ({ root, basePath, onOpenFile, searchForFile, refreshTok
searchForFile={searchForFile} searchForFile={searchForFile}
depth={0} depth={0}
refreshToken={refreshToken} refreshToken={refreshToken}
revealPath={revealPath}
/> />
))} ))}
</div> </div>