/** * The in-app workspace-directory browser (figma Harness 813-23126 family): a * 600×420 dialog (clamped to short/narrow viewports — the Miller row scrolls * sideways, the columns scroll down) whose header carries the title, the selection-path * breadcrumb, and a click-to-edit path zone; below it a Miller view — one * full-width level until a row is selected, then two 256px columns (level | * selected folder's children) around a hairline divider. Selecting in the * right column shifts the view one level deeper. "New folder" opens a nested * create dialog targeting the selected folder (or the level itself) and * selects the created folder. Open adopts the selected folder, falling back * to the listed level. Pure consumer of the injected browse calls — the * owning flow decides what "Open" means and owns the workspace-creation * error surface. Hidden entries are host-flagged and filtered here (a * show-hidden toggle is deferred work, client-side only). */ import { useCallback, useEffect, useRef, useState } from 'react' import clsx from 'clsx' import { Button, IconChevronRightOutline14, IconFolderClose16, IconFolderOpen16, IconPlusOutline16, Modal, } from '@deepseek-ai/dsh-client-ui-primitives' import type { DirectoryEntry, DirectoryListing } from '@deepseek-ai/dsh-client-runtime/client' import { DirectoryBrowseError } from '@deepseek-ai/dsh-client-runtime/client' import type { Translate } from '@deepseek-ai/dsh-client-locale/client' import css from './DirectoryBrowser.module.css' /** Owner-supplied browser props: browse calls, pick semantics, and copy. */ export interface DirectoryBrowserProps { /** Dialog visibility (owner-local; closed unmounts nothing but resets on reopen). */ open: boolean /** List one directory level (absent path = the Host home directory). */ listDirectory: (path?: string) => Promise /** Create one child directory under an existing parent. */ createDirectory: (path: string, name: string) => Promise /** The operator confirmed a directory (the selection, else the listed level). */ onOpen: (path: string) => void /** Close without picking (mask, Escape, Cancel). */ onClose: () => void /** The owner's confirm is in flight: Open disables, the view freezes. */ busy: boolean /** Localized copy. */ t: Translate } /** Failure text: the Host business message when typed, else the throw's text. */ function failureText(error: unknown): string { if (error instanceof DirectoryBrowseError) return error.rpcError.message return error instanceof Error ? error.message : String(error) } /** * Breadcrumb rows for display: inside the home subtree the chain starts at a * localized Home crumb; outside it the full ancestry shows, the root labeled * by its own path. */ function displayCrumbs(listing: DirectoryListing, homeLabel: string): DirectoryEntry[] { const homeIndex = listing.crumbs.findIndex(crumb => crumb.path === listing.home) if (homeIndex === -1) return listing.crumbs const tail = listing.crumbs.slice(homeIndex + 1) return [{ name: homeLabel, path: listing.home, hidden: false }, ...tail] } /** One column of folder rows (the Miller view renders one or two of these). */ function LevelColumn({ entries, selectedPath, busy, onPick, wide }: { entries: readonly DirectoryEntry[] selectedPath: string | null busy: boolean onPick: (entry: DirectoryEntry) => void wide: boolean }) { return (
{entries.filter(entry => !entry.hidden).map((entry) => { const selected = entry.path === selectedPath return ( // The wrapper carries the list semantics; the row keeps its NATIVE // button role so assistive technology exposes an actionable control. ) })}
) } /** * Render the directory-browser dialog. * @param props - owner-controlled browser props. * @returns the dialog element (null while closed, via Modal). */ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen, onClose, busy, t }: DirectoryBrowserProps) { // Miller state: the listed level, the selected row in it, and the selected // folder's own listing (the right column; null while nothing is selected). const [parent, setParent] = useState(null) const [selected, setSelected] = useState(null) const [child, setChild] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) // Path-edit state: null = breadcrumb mode; a string = the draft being typed. const [pathDraft, setPathDraft] = useState(null) // Create-folder state: null = closed; a string = the nested dialog's draft. const [folderDraft, setFolderDraft] = useState(null) const [creatingFolder, setCreatingFolder] = useState(false) const [createError, setCreateError] = useState(null) const requestSeq = useRef(0) // Bumped on every open/close edge: settlements from a previous open (a // pending creation included) must never mutate a reopened dialog. const openGeneration = useRef(0) // Deep ancestry overflows the trail; keep its tail (the current directory // and the edit zone beside it) in view whenever the chain changes. const crumbTrailRef = useRef(null) // IME confirmation (Enter selecting a candidate) must not submit either // text input; the same guard the workspace-name inputs carry, shared by // the path editor and the folder-name input. const composingRef = useRef(false) // HMR/unmount invalidation: a completion from a disposed flow must not // update state or issue follow-up requests from a dead component. useEffect(() => () => { requestSeq.current += 1 openGeneration.current += 1 }, []) const compositionGuard = { onCompositionStart: () => { composingRef.current = true }, onCompositionEnd: () => { composingRef.current = false }, } /** Replace the whole view with one freshly listed level (no selection). */ const navigate = useCallback((path?: string) => { const seq = ++requestSeq.current setLoading(true) setError(null) listDirectory(path).then((next) => { if (seq !== requestSeq.current) return setParent(next) setSelected(null) setChild(null) setLoading(false) setPathDraft(null) }, (reason: unknown) => { if (seq !== requestSeq.current) return setLoading(false) setError(failureText(reason)) }) }, [listDirectory]) /** Select a row of the listed level and preview its children on the right. */ const select = useCallback((entry: DirectoryEntry) => { const seq = ++requestSeq.current setSelected(entry) setChild(null) setLoading(true) setError(null) listDirectory(entry.path).then((next) => { if (seq !== requestSeq.current) return setChild(next) setLoading(false) }, (reason: unknown) => { if (seq !== requestSeq.current) return setLoading(false) setError(failureText(reason)) // An unreadable selection cannot be the committing target while the // breadcrumb still names the level: fall back to the single pane. setSelected(null) }) }, [listDirectory]) /** A right-column pick advances the view one level: child becomes the level. */ const advance = useCallback((entry: DirectoryEntry) => { /* v8 ignore next -- narrowing guard: the right column only renders with a child listing. */ if (child === null) return setParent(child) select(entry) }, [child, select]) // Every open starts fresh at the Host home directory; closing invalidates // any in-flight response so a late arrival cannot repopulate a closed dialog. useEffect(() => { openGeneration.current += 1 if (open) { setParent(null) setSelected(null) setChild(null) setCreatingFolder(false) navigate() return } requestSeq.current += 1 setError(null) setPathDraft(null) setFolderDraft(null) setCreateError(null) }, [open, navigate]) /** The folder a create or Open acts on: the selection, else the listed level. */ const targetPath = selected?.path ?? parent?.path ?? null const targetName = selected?.name ?? (parent === null ? '' : (displayCrumbs(parent, t('browser.home')).at(-1)?.name ?? parent.path)) const confirmCreate = (): void => { /* v8 ignore next -- reentry fence: the nested dialog only renders with a target and disables while creating. */ if (targetPath === null || folderDraft === null || creatingFolder) return const name = folderDraft.trim() if (name === '') return setCreatingFolder(true) setCreateError(null) const generation = openGeneration.current createDirectory(targetPath, name).then((createdPath) => { // A settlement from a closed (possibly reopened) flow must not touch // the fresh dialog or issue a relist against the stale target. if (generation !== openGeneration.current) return setCreatingFolder(false) setFolderDraft(null) // Land like a right-column pick (figma 802:57446 → 813:23278 flow): the // create target becomes the listed level and the new folder its selection. const seq = ++requestSeq.current setLoading(true) listDirectory(targetPath).then((level) => { /* v8 ignore next -- same fence as navigate/select; the modal blocks superseding input */ if (seq !== requestSeq.current) return setParent(level) setLoading(false) select({ name, path: createdPath, hidden: false }) }, (reason: unknown) => { /* v8 ignore next -- same fence as navigate/select; the modal blocks superseding input */ if (seq !== requestSeq.current) return setLoading(false) setError(failureText(reason)) }) }, (reason: unknown) => { if (generation !== openGeneration.current) return setCreatingFolder(false) setCreateError(failureText(reason)) }) } // After the hooks: a closed dialog renders nothing and evaluates no copy. const crumbSource = child ?? parent const crumbs = crumbSource === null ? [] : displayCrumbs(crumbSource, t('browser.home')) const crumbTail = crumbs.at(-1)?.path useEffect(() => { const trail = crumbTrailRef.current if (trail !== null) trail.scrollLeft = trail.scrollWidth }, [crumbTail]) // On viewports too narrow for both fixed panes the Miller row scrolls; // whenever a child preview lands, pin it into view the way the crumb tail // pins — otherwise descent is unreachable on a phone-width window. const millerRowRef = useRef(null) const childPath = child?.path useEffect(() => { const row = millerRowRef.current if (row !== null && childPath !== undefined) row.scrollLeft = row.scrollWidth }, [childPath]) if (!open) return null const twoPane = selected !== null // The nested create dialog owns the interaction while open: Modal has no // focus trap, so every parent control goes inert (Shift-Tab or AT must not // close, adopt, or retarget underneath the child). const parentInert = busy || folderDraft !== null // An uncommitted path draft makes targetPath stale relative to the header: // committing actions must not act on the previous selection/listing while // a different path is displayed. const draftPending = pathDraft !== null return ( { if (folderDraft === null && !busy) onClose() }} title={t('browser.title')} className={clsx(css.dialog)} headless >

{t('browser.title')}

{pathDraft === null ? ( <> {crumbs.map((crumb, index) => ( {index > 0 && } ))} {/* The empty zone right of the crumbs is the path-edit affordance. */}
{parent !== null && ( )} {twoPane && } {twoPane && child !== null && ( )}
{loading &&
{t('browser.loading')}
} {/* The backend bounds a level at its complete-result limit; say so * whenever a visible pane was cut instead of letting the tail of a * huge directory go silently missing. */} {(parent?.truncated === true || child?.truncated === true) && !loading &&
{t('browser.truncated')}
} {error !== null &&
{error}
}
{/* Nested create dialog (figma 813:23278): names one folder inside the target. */} { if (!creatingFolder) setFolderDraft(null) }} title={t('browser.newFolder')} className={clsx(css.createDialog)} headless >

{t('browser.newFolder')}

{t('browser.createIn', { name: targetName })}

{ setFolderDraft(event.target.value) }} {...compositionGuard} onKeyDown={(event) => { if (event.key === 'Enter' && !composingRef.current) { event.preventDefault() confirmCreate() } if (event.key === 'Escape') { event.stopPropagation() if (!creatingFolder) setFolderDraft(null) } }} /> {createError !== null &&
{createError}
}
) }