project-file-tree-refresh.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. import { listDirectory } from "@/commands/fs"
  2. import { useWikiStore } from "@/stores/wiki-store"
  3. import { normalizePath } from "@/lib/path-utils"
  4. export interface RefreshProjectFileTreeOptions {
  5. projectId?: string
  6. clearDisplayTreeFirst?: boolean
  7. bumpDataVersion?: boolean
  8. refreshDisplayTree?: boolean
  9. refreshPathIndex?: boolean
  10. }
  11. async function listDirectoryWithRetry(
  12. path: string,
  13. options: Parameters<typeof listDirectory>[1],
  14. attempts = 3,
  15. ): Promise<Awaited<ReturnType<typeof listDirectory>>> {
  16. let lastError: unknown
  17. for (let i = 0; i < attempts; i += 1) {
  18. try {
  19. return await listDirectory(path, options)
  20. } catch (err) {
  21. lastError = err
  22. if (i < attempts - 1) {
  23. await new Promise((resolve) => setTimeout(resolve, 250))
  24. }
  25. }
  26. }
  27. throw lastError
  28. }
  29. function isStillCurrentProject(projectId: string | undefined, projectPath: string): boolean {
  30. const current = useWikiStore.getState().project
  31. if (!current) return false
  32. if (projectId && current.id !== projectId) return false
  33. return normalizePath(current.path) === projectPath
  34. }
  35. /**
  36. * Reload the project file tree and optionally rebuild the path index.
  37. * QMAI keeps a full tree for KnowledgeTree (flatten/sort/chapter batch),
  38. * unlike llm-wiki's shallow display tree + lazy children.
  39. */
  40. export async function refreshProjectFileTree(
  41. projectPath: string,
  42. options: RefreshProjectFileTreeOptions = {},
  43. ): Promise<void> {
  44. const normalizedProjectPath = normalizePath(projectPath)
  45. const currentProjectId = options.projectId ?? useWikiStore.getState().project?.id
  46. const refreshDisplayTree = options.refreshDisplayTree ?? true
  47. const refreshPathIndex = options.refreshPathIndex ?? true
  48. if (options.clearDisplayTreeFirst && isStillCurrentProject(currentProjectId, normalizedProjectPath)) {
  49. useWikiStore.getState().setFileTree([], { syncPathIndex: false })
  50. }
  51. if (refreshDisplayTree) {
  52. try {
  53. const tree = await listDirectoryWithRetry(normalizedProjectPath, {}, 3)
  54. if (isStillCurrentProject(currentProjectId, normalizedProjectPath)) {
  55. useWikiStore.getState().setFileTree(tree, { syncPathIndex: refreshPathIndex })
  56. }
  57. } catch (err) {
  58. console.error("Failed to refresh project file tree:", err)
  59. }
  60. } else if (refreshPathIndex) {
  61. if (!isStillCurrentProject(currentProjectId, normalizedProjectPath)) return
  62. listDirectoryWithRetry(normalizedProjectPath, {}, 3)
  63. .then((fullTree) => {
  64. if (!isStillCurrentProject(currentProjectId, normalizedProjectPath)) return
  65. useWikiStore.getState().setProjectPathIndexFromTree(fullTree)
  66. })
  67. .catch((err) => {
  68. console.error("Failed to refresh project path index:", err)
  69. })
  70. }
  71. if (options.bumpDataVersion && isStillCurrentProject(currentProjectId, normalizedProjectPath)) {
  72. useWikiStore.getState().bumpDataVersion()
  73. }
  74. }