@@ -48,6 +48,8 @@ export function Outline({ width }: { width: number }) {
4848 > ( undefined ) ;
4949 const loadingViewIdsRef = useRef < Set < string > > ( new Set ( ) ) ;
5050 const autoLoadRetryAfterRef = useRef < Map < string , number > > ( new Map ( ) ) ;
51+ const validatingRestoreIdsRef = useRef < Set < string > > ( new Set ( ) ) ;
52+ const validatedExistingRestoreIdsRef = useRef < Set < string > > ( new Set ( ) ) ;
5153 const [ loadingRevision , setLoadingRevision ] = useState ( 0 ) ;
5254 const [ nowMs , setNowMs ] = useState ( ( ) => Date . now ( ) ) ;
5355 const loadingViewIds = useMemo ( ( ) => loadingViewIdsRef . current , [ loadingRevision ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
@@ -61,9 +63,61 @@ export function Outline({ width }: { width: number }) {
6163 setPendingAutoLoadIds ( restoredExpandedIds ) ;
6264 loadingViewIdsRef . current = new Set ( ) ;
6365 autoLoadRetryAfterRef . current = new Map ( ) ;
66+ validatingRestoreIdsRef . current = new Set ( ) ;
67+ validatedExistingRestoreIdsRef . current = new Set ( ) ;
6468 setLoadingRevision ( ( r ) => r + 1 ) ;
6569 } , [ currentWorkspaceId ] ) ;
6670
71+ // Validate restored expanded IDs that are not in the current tree and prune only truly stale IDs.
72+ // This avoids keeping deleted/moved IDs forever, while preserving valid deep IDs.
73+ useEffect ( ( ) => {
74+ if ( ! outline || outline . length === 0 || ! loadViewChildrenBatch || pendingAutoLoadIds . length === 0 ) return ;
75+
76+ const unknownIds = pendingAutoLoadIds . filter ( ( id ) => {
77+ if ( findView ( outline , id ) ) return false ;
78+ if ( validatedExistingRestoreIdsRef . current . has ( id ) ) return false ;
79+ if ( validatingRestoreIdsRef . current . has ( id ) ) return false ;
80+ return true ;
81+ } ) ;
82+
83+ if ( unknownIds . length === 0 ) return ;
84+
85+ unknownIds . forEach ( ( id ) => validatingRestoreIdsRef . current . add ( id ) ) ;
86+
87+ void loadViewChildrenBatch ( unknownIds )
88+ . then ( ( views ) => {
89+ const existingIds = new Set ( ( views || [ ] ) . map ( ( view ) => view . view_id ) ) ;
90+ const staleIds = unknownIds . filter ( ( id ) => ! existingIds . has ( id ) ) ;
91+
92+ existingIds . forEach ( ( id ) => validatedExistingRestoreIdsRef . current . add ( id ) ) ;
93+
94+ if ( staleIds . length === 0 ) return ;
95+
96+ const staleSet = new Set ( staleIds ) ;
97+
98+ staleIds . forEach ( ( id ) => {
99+ setOutlineExpands ( id , false ) ;
100+ loadingViewIdsRef . current . delete ( id ) ;
101+ autoLoadRetryAfterRef . current . delete ( id ) ;
102+ } ) ;
103+
104+ setPendingAutoLoadIds ( ( prev ) => {
105+ const next = prev . filter ( ( id ) => ! staleSet . has ( id ) ) ;
106+
107+ return next . length === prev . length ? prev : next ;
108+ } ) ;
109+ setExpandViewIds ( ( prev ) => {
110+ const next = prev . filter ( ( id ) => ! staleSet . has ( id ) ) ;
111+
112+ return next . length === prev . length ? prev : next ;
113+ } ) ;
114+ setLoadingRevision ( ( r ) => r + 1 ) ;
115+ } )
116+ . finally ( ( ) => {
117+ unknownIds . forEach ( ( id ) => validatingRestoreIdsRef . current . delete ( id ) ) ;
118+ } ) ;
119+ } , [ outline , pendingAutoLoadIds , loadViewChildrenBatch ] ) ;
120+
67121 // Drop startup pending ids as soon as they are confirmed loaded.
68122 useEffect ( ( ) => {
69123 setPendingAutoLoadIds ( ( prev ) => {
0 commit comments