/** * The files window's tree surface: a global file-name search box on top * (300ms debounce; an in-flight search is aborted by the next keystroke) * over either the shared controlled FileTree (empty query) or the flat * result list (relative paths; click opens through the caller's mode-aware * open). Owns its refresh tick: the icon next to the search input clears * the tree cache. EditorHost docks it as the tab's right panel (wrapped in * a drag-resize handle) and provides the file context-menu open escapes. */ import { useEffect, useState } from 'react' import clsx from 'clsx' import { IconRefreshOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import { api } from './api.ts' import { FileTree } from './FileTree.tsx' import { t } from './locales.ts' import { resolveSidebarPath } from './produced-files.ts' import css from './sidebar.module.css' export function TreePanel(props: { sessionId: string cwd: string | undefined expanded: string[] onToggle: (path: string) => void onOpenFile: (path: string) => void /** File context-menu "open in a new tab" (passed through to FileTree). */ onOpenFileNewTab?: (path: string) => void /** File context-menu "open to the side" (passed through to FileTree). */ onOpenFileSide?: (path: string) => void onReferenceFile: (path: string) => void /** Full-window presentation: the panel fills its host instead of docking * at a fixed width. */ full?: boolean }) { const { sessionId, cwd, expanded, onToggle, onOpenFile, onOpenFileNewTab, onOpenFileSide, onReferenceFile, full } = props const [query, setQuery] = useState('') const [results, setResults] = useState<{ matches: string[]; truncated: boolean } | null>(null) const [error, setError] = useState(null) const [refreshTick, setRefreshTick] = useState(0) const needle = query.trim() useEffect(() => { if (needle === '') { setResults(null) setError(null) return } const controller = new AbortController() const timer = window.setTimeout(() => { api.fsSearch({ sessionId, cwd }, needle, controller.signal).then((found) => { setResults(found) setError(null) }).catch((failure: unknown) => { if (controller.signal.aborted) return setResults(null) setError(failure instanceof Error ? failure.message : String(failure)) }) }, 300) return () => { window.clearTimeout(timer) controller.abort() } }, [sessionId, cwd, needle]) return (
{ setQuery(event.target.value) }} />
{needle === '' ? ( ) : (
{error !== null &&
{error}
} {error === null && results === null &&
{t('loading')}
} {error === null && results !== null && results.matches.length === 0 && (
{t('editorSearchNoResults')}
)} {error === null && results !== null && results.matches.map(rel => ( ))} {error === null && results?.truncated === true && (
{t('editorSearchTruncated')}
)}
)}
) }