({ children }: WorkspaceProviderProps)
| 340 | } |
| 341 | |
| 342 | export function WorkspaceProvider({ children }: WorkspaceProviderProps) { |
| 343 | const t = useTranslations("Folder.workspaceContext") |
| 344 | const { activeFolder } = useActiveFolder() |
| 345 | const { getFolder, allFolders } = useAppWorkspace() |
| 346 | const folderPath = activeFolder?.path |
| 347 | const [activePane, setActivePaneState] = |
| 348 | useState<WorkspacePane>("conversation") |
| 349 | const [fileTabs, setFileTabs] = useState<FileWorkspaceTab[]>([]) |
| 350 | const [activeFileTabId, setActiveFileTabId] = useState<string | null>(null) |
| 351 | const [pendingFileReveal, setPendingFileReveal] = useState<{ |
| 352 | requestId: number |
| 353 | path: string |
| 354 | line: number |
| 355 | } | null>(null) |
| 356 | const [previewFileTabIds, setPreviewFileTabIds] = useState<Set<string>>( |
| 357 | new Set() |
| 358 | ) |
| 359 | const [filesMaximized, setFilesMaximized] = useState(false) |
| 360 | // FIFO queue of unresolved disk-vs-buffer divergences (head is shown by |
| 361 | // the always-mounted conflict dialog). Isolated state: never flows into |
| 362 | // the fileTabs slice, so idle cost is zero. |
| 363 | const [externalConflictQueue, setExternalConflictQueue] = useState< |
| 364 | WorkspaceExternalConflict[] |
| 365 | >([]) |
| 366 | const externalConflictQueueRef = useRef<WorkspaceExternalConflict[]>([]) |
| 367 | // key(folderId,path) -> last announced signature. Suppresses re-prompt |
| 368 | // flicker when repeated events report the same divergence. |
| 369 | const conflictSignatureByKeyRef = useRef<Map<string, string>>(new Map()) |
| 370 | // key(folderId,path) -> etag of our own most recent save (one-shot). |
| 371 | const selfWriteEchoRef = useRef<Map<string, { etag: string; at: number }>>( |
| 372 | new Map() |
| 373 | ) |
| 374 | const fileTabsRef = useRef<FileWorkspaceTab[]>([]) |
| 375 | // Latest-state mirrors for the stable action callbacks. Actions live in a |
| 376 | // context value that must NOT change identity when tabs/folder change, so |
| 377 | // they read these refs instead of capturing render-scoped state. The refs |
| 378 | // are synced in effects (post-commit), giving the same staleness window a |
| 379 | // recreated closure would have had — never fresher, never older. |
| 380 | const activeFileTabIdRef = useRef<string | null>(null) |
| 381 | const activeFolderRef = useRef<{ id: number; path: string } | null>(null) |
| 382 | const getFolderRef = useRef(getFolder) |
| 383 | const allFoldersRef = useRef(allFolders) |
| 384 | const fileRevealRequestIdRef = useRef(0) |
| 385 | // tabId -> generation of its current in-flight fetch. Serves two roles: |
| 386 | // (a) Dedup: `has(tabId)` collapses rapid re-clicks within one event |
| 387 | // loop turn (where fileTabsRef.current is still pre-render-stale). |
| 388 | // (b) Staleness check: each fetch captures the generation it was |
| 389 | // started with and only commits state on resolve if it still |
| 390 | // matches — preventing an orphaned fetch (after close+reopen, or |
| 391 | // a superseding refresh) from clobbering the tab. |
| 392 | const inFlightLoadsRef = useRef<Map<string, number>>(new Map()) |
| 393 | const nextLoadGenRef = useRef(0) |
| 394 | // Most-recently-active tab ids, most recent first. Drives the memory |
| 395 | // guardrail's least-recently-active eviction order. |
| 396 | const tabRecencyRef = useRef<string[]>([]) |
| 397 | |
| 398 | useEffect(() => { |
| 399 | fileTabsRef.current = fileTabs |
nothing calls this directly
no test coverage detected