返回 DeepSeek-Reasonix
workspaceRefreshInvalidation.ts
根目录 / desktop / frontend / src / lib / workspaceRefreshInvalidation.ts
1 import { useEffect, useRef } from "react";
2 import type { WorkspaceRefreshSnapshot } from "./workspaceRefreshStore";
3 import type { WorkspaceRefreshScheduler } from "./workspaceRefreshScheduler";
4
5 type SchedulerRef = { current: WorkspaceRefreshScheduler | null };
6 type Load = () => Promise<void> | void;
7
8 interface WorkspaceRefreshInvalidationOptions {
9 commitHistoryOpen: boolean;
10 filter: string;
11 gitMetaSchedulerRef: SchedulerRef;
12 loadChangeDetail: Load;
13 loadDir: (dir: string) => unknown;
14 loadGitHistory: Load;
15 loadWorkspaceChanges: Load;
16 open: boolean;
17 openDirsRef: { current: Set<string> };
18 refreshSelected: () => unknown;
19 deferSelectedRefresh?: boolean;
20 onSelectedInvalidated?: () => void;
21 selectedPath: string | null;
22 setSearchResults: (value: null) => void;
23 viewMode: string;
24 workingTreeSchedulerRef: SchedulerRef;
25 workspaceRefresh: WorkspaceRefreshSnapshot;
26 workspaceScopeKey: string;
27 }
28
29 export interface WorkspaceRefreshActions {
30 forceVisible: boolean;
31 content: boolean;
32 tree: boolean;
33 workingTree: boolean;
34 gitMeta: boolean;
35 }
36
37 export function workspaceRefreshActions(
38 previous: WorkspaceRefreshSnapshot["revisions"],
39 next: WorkspaceRefreshSnapshot,
40 commitHistoryOpen: boolean,
41 ): WorkspaceRefreshActions {
42 // Focus reconciliation is also the recovery path for authorized paths that
43 // intentionally live outside the watched root. Watcher failures publish an
44 // all-paths degraded event even when no resource revision could be advanced.
45 const forceVisible = next.allPaths && (next.source === "reconcile" || next.watchState !== "active");
46 return {
47 forceVisible,
48 content: next.revisions.content > previous.content || forceVisible,
49 tree: next.revisions.tree > previous.tree || forceVisible,
50 workingTree: next.revisions.workingTree > previous.workingTree || next.revisions.session > previous.session || forceVisible,
51 gitMeta: next.revisions.gitMeta > previous.gitMeta || (forceVisible && commitHistoryOpen),
52 };
53 }
54
55 export function workspaceRefreshFallbackSequence(snapshot: WorkspaceRefreshSnapshot): number {
56 return snapshot.allPaths && (snapshot.source === "reconcile" || snapshot.watchState !== "active") ? snapshot.sequence : 0;
57 }
58
59 function parentDirs(path: string): string[] {
60 const parts = path.split("/").filter(Boolean);
61 const dirs = [""];
62 let acc = "";
63 for (let i = 0; i < parts.length - 1; i++) {
64 acc += `${parts[i]}/`;
65 dirs.push(acc);
66 }
67 return dirs;
68 }
69
70 export function useWorkspaceRefreshInvalidation({
71 commitHistoryOpen,
72 filter,
73 gitMetaSchedulerRef,
74 loadChangeDetail,
75 loadDir,
76 loadGitHistory,
77 loadWorkspaceChanges,
78 open,
79 openDirsRef,
80 refreshSelected,
81 deferSelectedRefresh = false,
82 onSelectedInvalidated,
83 selectedPath,
84 setSearchResults,
85 viewMode,
86 workingTreeSchedulerRef,
87 workspaceRefresh,
88 workspaceScopeKey,
89 }: WorkspaceRefreshInvalidationOptions): void {
90 const lastSequenceRef = useRef(workspaceRefresh.sequence);
91 const lastRevisionsRef = useRef(workspaceRefresh.revisions);
92 const lastScopeRef = useRef(workspaceScopeKey);
93
94 useEffect(() => {
95 if (!open) return;
96 if (lastScopeRef.current !== workspaceScopeKey) {
97 lastScopeRef.current = workspaceScopeKey;
98 lastSequenceRef.current = workspaceRefresh.sequence;
99 lastRevisionsRef.current = workspaceRefresh.revisions;
100 return;
101 }
102 if (lastSequenceRef.current === workspaceRefresh.sequence) return;
103 const previous = lastRevisionsRef.current;
104 lastSequenceRef.current = workspaceRefresh.sequence;
105 lastRevisionsRef.current = workspaceRefresh.revisions;
106 const { changes } = workspaceRefresh;
107 const actions = workspaceRefreshActions(previous, workspaceRefresh, commitHistoryOpen);
108 const affectsSelected = workspaceRefresh.allPaths || !selectedPath || changes.some((change) =>
109 change.path === selectedPath || change.oldPath === selectedPath || selectedPath.startsWith(`${change.path}/`),
110 );
111 if (actions.content && (actions.forceVisible || affectsSelected) && selectedPath) {
112 if (deferSelectedRefresh) onSelectedInvalidated?.();
113 else void refreshSelected();
114 }
115 if (actions.tree && (actions.forceVisible || workspaceRefresh.allPaths || changes.length > 0)) {
116 const affectedDirs = workspaceRefresh.allPaths
117 ? openDirsRef.current
118 : new Set(changes.flatMap((change) => [change.path, change.oldPath].filter(Boolean).flatMap((path) => parentDirs(path as string))));
119 for (const dir of affectedDirs) {
120 if (openDirsRef.current.has(dir)) void loadDir(dir);
121 }
122 if (filter.trim()) setSearchResults(null);
123 }
124 if (viewMode === "changed") {
125 if (actions.workingTree) {
126 workingTreeSchedulerRef.current?.trigger(async () => {
127 await Promise.all([loadWorkspaceChanges(), selectedPath ? loadChangeDetail() : Promise.resolve()]);
128 });
129 }
130 if (actions.gitMeta) gitMetaSchedulerRef.current?.trigger(loadGitHistory);
131 }
132 }, [
133 filter,
134 commitHistoryOpen,
135 gitMetaSchedulerRef,
136 loadChangeDetail,
137 loadDir,
138 loadGitHistory,
139 loadWorkspaceChanges,
140 open,
141 openDirsRef,
142 refreshSelected,
143 deferSelectedRefresh,
144 onSelectedInvalidated,
145 selectedPath,
146 setSearchResults,
147 viewMode,
148 workingTreeSchedulerRef,
149 workspaceRefresh,
150 workspaceScopeKey,
151 ]);
152 }
153
153 lines TYPESCRIPT