feat(file-explorer): lazy per-directory watchers + deferred teardown#112
Merged
Merged
Conversation
6 tasks
…loses ChengLabResearch#110) Replace the single recursive `Watcher(root, {recursive:true, depth:6, limit})` in `src/main/event-handlers/filesystem.ts` with a `Map<path, WatcherEntry>` of non-recursive watchers. `fetch-folder-contents(root)` closes all watchers, resets the aggregate visible-add counter, and starts a single watcher on the root - root is now just a distinguished expanded folder. Two new IPC handlers drive lazy loading: - `expand-folder(subPath)` starts a non-recursive watcher on `subPath`, or cancels a pending teardown if one is queued (idempotent open-close-open). - `collapse-folder(subPath)` schedules teardown of the subPath watcher after `COLLAPSE_TEARDOWN_DELAY_MS` (30_000 ms). Cancelled by a subsequent `expand-folder(subPath)`. Root is exempt - collapse-of-root is a no-op. `FILE_EXPLORER_WATCH_DEPTH` is removed; the lazy model makes it redundant. `FILE_EXPLORER_WATCH_LIMIT` is retained but its role changes from a per-watcher `watcher`-package limit to an aggregate session budget summed across every open watcher (each watcher's contribution is reclaimed on teardown). `src/shared/constants.ts` is a new dependency-free module that hosts `COLLAPSE_TEARDOWN_DELAY_MS` so both the main process and the renderer import the same value. `tsconfig.node.json` and `tsconfig.web.json` include it; `electron.vite.config.ts` adds an `@shared` alias for future reach. Renderer changes: - `DirectoryContext` exposes `expandFolder(subPath)` and `collapseFolder(subPath)`. Collapse mirrors the main-side timing with a state prune scheduled via `pruneSubtree(subPath)`; re-expanding within the window cancels both timers. Switching the root clears all pending timers. - `DraggableEntry` dispatches expand/collapse alongside its existing visual `isCollapsed` toggle. Idempotent - main-side handlers no-op when state already matches. The chevron now shows on every folder (not just non-empty ones) so an unopened lazy folder is expandable. - The `nodes: {}` IFrame broadcast is unchanged; no plugin-facing API in this PR (that lands in ChengLabResearch#111). `src/preload/index.ts` documents the new IPC channels; the electron-toolkit preload accepts arbitrary channel strings so no per-channel type is required. Docs: - `documentation/docs/index.md`: Large Folder Behavior rewritten for the lazy model. - `documentation/docs/reference/technical-constants.md`: `FILE_EXPLORER_WATCH_DEPTH` removed, `FILE_EXPLORER_WATCH_LIMIT` role clarified, `COLLAPSE_TEARDOWN_DELAY_MS` added with source, value, rationale, behavior at teardown, and when to change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tavateva
pushed a commit
to tavateva/ouroboros
that referenced
this pull request
Jul 7, 2026
…ntics (closes ChengLabResearch#111) Adds `request-directory-contents` / `send-directory-contents-response` message pair to the plugin iframe surface, so plugins can walk the current directory root on demand after ChengLabResearch#106 stopped broadcasting the full recursive tree. Renderer: - New valibot schemas in `src/renderer/src/schemas/iframe-message-schema.ts`: `RequestDirectoryContentsSchema` (plugin -> provider) and `SendDirectoryContentsResponseSchema` (provider -> requesting iframe). A recursive `PluginFileSystemNodeSchema` (via `lazy`) mirrors the renderer's `NodeChildren` shape so plugins get a drop-in replacement for the pre-ChengLabResearch#106 broadcast payload. - `IFrameContext.tsx` gains `handleRequestDirectoryContentsRequest` wired alongside the existing `read-file`/`save-file`/`register-plugin` handlers. It reads the current root through a ref so the closure sees the latest `directoryPath` regardless of when the listener effect ran, applies a path-traversal guard (rejects `..` escapes and Windows drive-letter changes), and posts the response only to the requesting iframe. Root broadcast (`send-directory-contents { nodes: {} }`) is unchanged. - Errors map to `denied` (traversal / no root / malformed), `not-found` (ENOENT / ENOTDIR), `limit` (enumeration truncated), or `internal` (anything else). Main: - `src/main/event-handlers/filesystem.ts` gains a `get-folder-contents` IPC handler backed by a top-level `getFolderContents` that does a one-shot `readdir` walk. Recursion is depth-first and bounded by the same `FILE_EXPLORER_WATCH_LIMIT` used by the watcher path; hitting the cap returns `{ nodes, truncated: true }` with the partial result. - Existing watcher factory refactored to use a shared `shouldIgnorePathAgainstRoot` helper so both the watcher and the one-shot walk apply the same dotfile + `IGNORED_PATH_SEGMENTS` rule. - Handler does not touch the watcher-manager state; enumeration is independent of expand/collapse. Docs: - `documentation/docs/guide/plugins.md` gets a "Requesting Directory Contents" section with request/response examples, the error-code taxonomy, and notes on the traversal guard, truncation semantics, and ignore-rule inheritance. - `documentation/docs/reference/technical-constants.md` extends the `FILE_EXPLORER_WATCH_LIMIT` entry with its second role as the per-request enumeration cap, and points the "Plugin directory broadcast" section at the new on-demand API. `src/preload/index.ts` documents the new IPC channel alongside the existing `expand-folder` / `collapse-folder` entries. Verification: `npm run typecheck` passes (both node and web tsconfigs). `npm run lint` still fails with the pre-existing ESLint 9 config drift called out in ChengLabResearch#109/ChengLabResearch#112, not introduced by this PR. Path-traversal in the existing `read-file` / `save-file` handlers is unrelated to ChengLabResearch#111 and left as a follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tavateva
force-pushed
the
lazy-file-explorer
branch
from
July 7, 2026 13:59
15555f2 to
c3f2d9d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #110. Refs #102.
Summary
Replaces the single recursive
Watcher(root, {recursive:true, depth:6, limit})in
src/main/event-handlers/filesystem.tswith aMap<path, WatcherEntry>ofnon-recursive watchers.
fetch-folder-contents(root)closes every openwatcher, resets the aggregate visible-add counter, and starts one watcher on
the root - root is now just a distinguished expanded folder. Two new IPC
handlers drive lazy loading:
expand-folder(subPath)starts a non-recursive watcher onsubPath(orcancels a pending teardown if one is queued, making open-close-open
idempotent).
collapse-folder(subPath)schedules teardown of the subPath watcher afterCOLLAPSE_TEARDOWN_DELAY_MS(30_000 ms). Cancelled by a subsequentexpand-folder(subPath). Root is exempt; collapse-of-root is a no-op.FILE_EXPLORER_WATCH_DEPTHis removed.FILE_EXPLORER_WATCH_LIMITis keptbut its role changes from a per-watcher
watcher-package limit to anaggregate session budget summed across every open watcher; a watcher's
contribution is reclaimed on teardown.
src/shared/constants.tsis a new dependency-free module holdingCOLLAPSE_TEARDOWN_DELAY_MSso both the main process and the renderer importthe same value.
tsconfig.node.jsonandtsconfig.web.jsoninclude it;electron.vite.config.tsgets an@sharedalias.Renderer:
DirectoryContextexposesexpandFolder(subPath)andcollapseFolder(subPath).Collapse mirrors the main-side timing with a state prune scheduled via
pruneSubtree(subPath); re-expanding within the window cancels both timers.Switching the root clears all pending timers.
DraggableEntrydispatches expand/collapse alongside its existing visualisCollapsedtoggle. Idempotent - main-side handlers no-op when statealready matches. The chevron now renders on every folder (not just
non-empty ones) so an unopened lazy folder is expandable.
nodes: {}IFrame broadcast is unchanged; no plugin-facing API inthis PR (that lands in Plugin-facing directory request API with recursive semantics (part of #102) #111).
src/preload/index.tsdocuments the new IPC channels; the electron-toolkitpreload accepts arbitrary channel strings so no per-channel type is required.
Acceptance criteria (from #110)
direct entries. (Root is watched non-recursively; batches only include
root's direct children until subfolders are expanded.)
recursion. (New non-recursive watcher per expanded folder.)
re-expanded first. (Both main-side watcher teardown and renderer-side
pruneSubtreeare scheduled onCOLLAPSE_TEARDOWN_DELAY_MSand arecancelled by a subsequent expand.)
(
FileExplorer.tsx:80isUsed) continue to work. (No changes to theNodeChildrenshape;handleFSEventstill populates the nested map frombatch events, only lazily.)
npm run typecheckpasses. (Verified locally; see below.)documentation/docs/index.md(Large Folder Behavior)and
documentation/docs/reference/technical-constants.md(
COLLAPSE_TEARDOWN_DELAY_MSadded;FILE_EXPLORER_WATCH_DEPTHremoved;FILE_EXPLORER_WATCH_LIMITrole clarified).Design notes / minor divergences
DraggableEntryhid thechevron for empty folders (
!isEmpty). Under lazy loading a folder startswith
children: {}until first expand, so!isEmptywould hide the chevronon every not-yet-expanded folder and there would be no way to expand it.
The chevron now shows on every folder regardless. True empty folders are
still visually distinguished by the
emptyFolderstyle.handleEventsruns beforecreateWindow, sogetMainWindow()returns undefined athandler-registration time. The window
closedlistener is bound lazily onthe first
fetch-folder-contentscall and rebinds if the window isrecreated (e.g. macOS reactivation).
addCountalso tracks unlinks. When anunlink/unlinkDirevent fires, both the per-watcher and aggregate countersdecrement. This keeps the aggregate budget honest across long-lived
sessions where files come and go without triggering false limit warnings.
pruneSubtreereducer. Renderer-side prune walks the nestednodesmap by absolute
pathequality (not by root-relative path decomposition)because
FileSystemNode.pathis already absolute; this keeps the reducerindependent of the current root and makes it work uniformly for any
subPath the main process might collapse.
Blockers / pre-existing issues
npm run lintfails withESLint couldn't find an eslint.config.(js|mjs|cjs) file.This is the pre-existing ESLint 9 config drift called out in PR chore(constants): named ports w/ env override, tighten iframe allowlist, split memory interval, env-configurable shm_size #109; not
introduced by this PR and not addressed here.
npm run devwas not attempted; the automation host hasno reachable display. A reviewer can run
npm run dev, open a mid-sizefolder, and expand/collapse a subfolder to visually confirm:
visible re-fetch flicker.
shows a fresh load.
Verification commands
Files changed
src/main/event-handlers/filesystem.ts- watcher-manager rewrite +expand-folder/collapse-folderhandlers.src/renderer/src/contexts/DirectoryContext.tsx- newexpandFolder/collapseFoldermethods,pruneSubtreereducer,timer bookkeeping.
src/renderer/src/components/MenuPanel/components/FileExplorer/components/DraggableEntry/DraggableEntry.tsx-dispatch expand/collapse on chevron click; always show chevron on folders.
src/shared/constants.ts- new;COLLAPSE_TEARDOWN_DELAY_MS.src/preload/index.ts- documenting comment for the new IPC channels.tsconfig.node.json,tsconfig.web.json,electron.vite.config.ts-include
src/shared/**/*and register the@sharedalias.documentation/docs/index.md- Large Folder Behavior rewritten for thelazy model.
documentation/docs/reference/technical-constants.md-FILE_EXPLORER_WATCH_DEPTHremoved,FILE_EXPLORER_WATCH_LIMITroleclarified,
COLLAPSE_TEARDOWN_DELAY_MSdocumented.