diff --git a/web/app/components/workflow/skill/file-tree/index.tsx b/web/app/components/workflow/skill/file-tree/index.tsx
index cefd5efe9b..cf2e60c9cf 100644
--- a/web/app/components/workflow/skill/file-tree/index.tsx
+++ b/web/app/components/workflow/skill/file-tree/index.tsx
@@ -28,6 +28,7 @@ import { useSkillTreeCollaboration } from '../hooks/use-skill-tree-collaboration
import { useSyncTreeWithActiveTab } from '../hooks/use-sync-tree-with-active-tab'
import { isDescendantOf } from '../utils/tree-utils'
import DragActionTooltip from './drag-action-tooltip'
+import SearchResultList from './search-result-list'
import TreeContextMenu from './tree-context-menu'
import TreeNode from './tree-node'
import UploadStatusTooltip from './upload-status-tooltip'
@@ -366,6 +367,17 @@ const FileTree = ({ className }: FileTreeProps) => {
)
}
+ if (searchTerm) {
+ return (
+
{
+ const activeTabId = useStore(s => s.activeTabId)
+ const storeApi = useWorkflowStore()
+
+ const results = useMemo(
+ () => flattenMatchingNodes(treeChildren, searchTerm),
+ [treeChildren, searchTerm],
+ )
+
+ const handleClick = useCallback((node: AppAssetTreeView) => {
+ if (node.node_type === 'file') {
+ storeApi.getState().openTab(node.id, { pinned: true })
+ }
+ else {
+ const ancestors = getAncestorIds(node.id, treeChildren)
+ storeApi.getState().revealFile([...ancestors, node.id])
+ storeApi.getState().setFileTreeSearchTerm('')
+ }
+ }, [storeApi, treeChildren])
+
+ return (
+
+ {results.map(({ node, parentPath }) => (
+
handleClick(node)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ handleClick(node)
+ }
+ }}
+ >
+
+
+
+
+
+ {node.name}
+
+
+ {parentPath && (
+
+ {parentPath}
+
+ )}
+
+ ))}
+
+ )
+}
+
+export default SearchResultList
diff --git a/web/app/components/workflow/skill/utils/tree-utils.ts b/web/app/components/workflow/skill/utils/tree-utils.ts
index 59dd02425e..d1076119e9 100644
--- a/web/app/components/workflow/skill/utils/tree-utils.ts
+++ b/web/app/components/workflow/skill/utils/tree-utils.ts
@@ -199,6 +199,37 @@ function insertDraftNodeAtParent(
return { nodes: inserted ? nextNodes : nodes, inserted }
}
+export type FlatSearchResult = {
+ node: AppAssetTreeView
+ parentPath: string
+}
+
+export function flattenMatchingNodes(
+ nodes: AppAssetTreeView[],
+ searchTerm: string,
+): FlatSearchResult[] {
+ if (!searchTerm)
+ return []
+
+ const results: FlatSearchResult[] = []
+ const lowerTerm = searchTerm.toLowerCase()
+
+ function traverse(nodeList: AppAssetTreeView[]): void {
+ for (const node of nodeList) {
+ if (node.name.toLowerCase().includes(lowerTerm)) {
+ const lastSlash = node.path.lastIndexOf('/')
+ const parentPath = lastSlash > 0 ? node.path.slice(1, lastSlash) : ''
+ results.push({ node, parentPath })
+ }
+ if (node.children && node.children.length > 0)
+ traverse(node.children)
+ }
+ }
+
+ traverse(nodes)
+ return results
+}
+
export function insertDraftTreeNode(
nodes: AppAssetTreeView[],
parentId: string | null,