feat(web): show hidden directories in workspace browser (#1331)

* feat(web): show hidden directories in workspace browser

Add optional includeHidden param to the machine list-directory RPC so the
WorkspaceBrowser can toggle hidden (dot-prefixed) entries. Default remains
filtered for backward compatibility; the toggle persists via localStorage.

* fix(web): disable show-hidden toggle while directory loading

Prevent overlapping list-directory requests with opposite includeHidden
values; the toggle is now disabled while a directory load is active.
This commit is contained in:
weixiang1862
2026-08-03 12:32:50 +08:00
committed by GitHub
parent a111d0bc82
commit cc39021abc
10 changed files with 138 additions and 21 deletions
+65 -12
View File
@@ -53,6 +53,22 @@ function RefreshIcon(props: { className?: string }) {
)
}
function CheckboxBlankIcon(props: { className?: string }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" className={props.className}>
<path d="M4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM5 5V19H19V5H5Z" />
</svg>
)
}
function CheckboxCheckedIcon(props: { className?: string }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" className={props.className}>
<path d="M4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM5 5V19H19V5H5ZM11.0026 16L6.75999 11.7574L8.17421 10.3431L11.0026 13.1716L16.6595 7.51472L18.0737 8.92893L11.0026 16Z" />
</svg>
)
}
function getMachineTitle(machine: Machine): string {
if (machine.metadata?.displayName) return machine.metadata.displayName
if (machine.metadata?.host) return machine.metadata.host
@@ -142,6 +158,16 @@ function buildBreadcrumbs(currentPath: string, root: string): { label: string; p
return crumbs
}
const SHOW_HIDDEN_STORAGE_KEY = 'hapi:workspaceBrowserShowHidden'
function readShowHidden(): boolean {
try {
return localStorage.getItem(SHOW_HIDDEN_STORAGE_KEY) === '1'
} catch {
return false
}
}
export function WorkspaceBrowser(props: {
api: ApiClient
machines: Machine[]
@@ -159,6 +185,7 @@ export function WorkspaceBrowser(props: {
const [entries, setEntries] = useState<MachineDirectoryEntry[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [showHidden, setShowHidden] = useState<boolean>(readShowHidden)
useEffect(() => {
if (machines.length === 0) {
@@ -190,12 +217,12 @@ export function WorkspaceBrowser(props: {
[selectedMachine?.metadata?.workspaceRoots]
)
const loadDirectory = useCallback(async (path: string) => {
const loadDirectory = useCallback(async (path: string, includeHiddenOverride?: boolean) => {
if (!machineId) return
setIsLoading(true)
setError(null)
try {
const result = await api.listMachineDirectory(machineId, path)
const result = await api.listMachineDirectory(machineId, path, { includeHidden: includeHiddenOverride ?? showHidden })
if (result.success && result.entries) {
setEntries(result.entries)
setCurrentPath(path)
@@ -212,7 +239,7 @@ export function WorkspaceBrowser(props: {
} finally {
setIsLoading(false)
}
}, [api, machineId, queryClient])
}, [api, machineId, queryClient, showHidden])
useEffect(() => {
if (workspaceRoots.length === 0) {
@@ -256,6 +283,16 @@ export function WorkspaceBrowser(props: {
if (currentPath) void loadDirectory(currentPath)
}, [currentPath, loadDirectory])
const handleToggleHidden = useCallback(() => {
const next = !showHidden
setShowHidden(next)
try {
localStorage.setItem(SHOW_HIDDEN_STORAGE_KEY, next ? '1' : '0')
} catch {
}
if (currentPath) void loadDirectory(currentPath, next)
}, [showHidden, currentPath, loadDirectory])
const handleStartSession = useCallback(() => {
if (!machineId || !currentPath) return
props.onStartSession(machineId, currentPath)
@@ -368,15 +405,31 @@ export function WorkspaceBrowser(props: {
</button>
</span>
))}
<button
type="button"
onClick={handleRefresh}
disabled={isLoading}
className="ml-auto shrink-0 p-0.5 rounded hover:bg-[var(--app-subtle-bg)] text-[var(--app-hint)] hover:text-[var(--app-fg)] transition-colors"
title={t('browse.refresh')}
>
<RefreshIcon className={`h-3.5 w-3.5 ${isLoading ? 'animate-spin' : ''}`} />
</button>
<div className="ml-auto shrink-0 flex items-center gap-1">
<button
type="button"
onClick={handleToggleHidden}
aria-pressed={showHidden}
disabled={isLoading}
className="shrink-0 flex items-center gap-1.5 rounded px-1.5 py-0.5 text-xs text-[var(--app-hint)] hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)] transition-colors disabled:pointer-events-none disabled:opacity-50"
>
{showHidden ? (
<CheckboxCheckedIcon className="h-3.5 w-3.5 text-[var(--app-link)] shrink-0" />
) : (
<CheckboxBlankIcon className="h-3.5 w-3.5 shrink-0" />
)}
{t('browse.showHidden')}
</button>
<button
type="button"
onClick={handleRefresh}
disabled={isLoading}
className="shrink-0 p-0.5 rounded hover:bg-[var(--app-subtle-bg)] text-[var(--app-hint)] hover:text-[var(--app-fg)] transition-colors"
title={t('browse.refresh')}
>
<RefreshIcon className={`h-3.5 w-3.5 ${isLoading ? 'animate-spin' : ''}`} />
</button>
</div>
</div>
)}
</div>