Skip to content

show empty folders in sidebar too#243

Draft
dimaMachina wants to merge 1 commit intomainfrom
show-empty-folders-in-sidebar
Draft

show empty folders in sidebar too#243
dimaMachina wants to merge 1 commit intomainfrom
show-empty-folders-in-sidebar

Conversation

@dimaMachina
Copy link
Copy Markdown
Contributor

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 21, 2026

⚠️ No Changeset found

Latest commit: 3dc621b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Copy Markdown

@inkeep-internal-ci inkeep-internal-ci Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(4) Total Issues | Risk: Medium

🟠⚠️ Major (2) 🟠⚠️

🟠 1) api-extension.ts:590-616 Synchronous disk walk blocks event loop on every request

Issue: The new collectFolderPathsFromDisk() function performs a synchronous recursive directory walk using readdirSync() on every GET /api/pages request. This blocks the Node.js event loop for the duration of the walk.

Why: /api/pages is called frequently: on initial load, on tab focus, on visibility change, and on CC1 files broadcasts. For large content directories, each call could block for 10-100ms, pausing all collaborative editing operations. The existing getFileIndex() uses an incremental pattern (populated at startup, updated on events), but this reintroduces sync disk I/O into the hot path.

Fix: Maintain the folder index incrementally in file-watcher.ts alongside fileIndex. Extend seedLastKnownHashes() to track folder paths at startup, then update incrementally on create/delete/rename DiskEvents. Expose via getFolderIndex(). Alternatively, cache with TTL invalidation if incremental is too complex for v1.

Refs: file-watcher.ts:395-560

🟠 2) api-extension.ts:603 Symlinked directories silently excluded

Issue: entry.isDirectory() returns false for symbolic links pointing to directories, causing symlinked folders to be invisible in the sidebar.

Why: CLAUDE.md states "Symlinks inside the content directory are fully supported." The existing file-watcher.ts explicitly handles symlinked directories (lines 419-460) by checking isSymbolicLink() first. This inconsistency means symlinked empty folders won't appear while symlinked files work correctly.

Fix: Check entry.isSymbolicLink(), resolve with realpathSync, validate target is within contentDir, and handle ENOENT/ELOOP. Follow the pattern in file-watcher.ts:seedLastKnownHashes().

Refs: file-watcher.ts:419-460

Inline Comments:

  • 🟠 Major: api-extension.ts:590-616 Synchronous disk walk blocks event loop
  • 🟠 Major: api-extension.ts:603 Symlinked directories excluded
  • 🟡 Minor: api-extension.ts:606 Incomplete fallback exclusions

🟡 Minor (1) 🟡

🟡 1) api-extension.ts:606 Fallback exclusions incomplete

Issue: excludedByFallback only checks .git and node_modules, but BUILTIN_SKIP_DIRS in content-filter.ts includes 16 more directories (dist, build, .next, etc.).

Why: When contentFilter is absent, empty build/cache directories would appear in the sidebar. Low impact since standalone.ts always provides contentFilter.

Fix: Import BUILTIN_SKIP_DIRS from content-filter.ts, or make contentFilter required.

Refs: content-filter.ts:39-62

💭 Consider (1) 💭

💭 1) api-extension.ts:596-600 Add structured error context to directory read failures

Issue: The error log doesn't include the error code (EACCES, ENOENT, ELOOP) in a structured way.

Why: During incident response when users report missing folders, operators need to quickly identify if it's a permission issue, broken symlink, or race condition.

Fix: Extract error code: console.warn('[api] Failed to read directory', { path: absDirPath, code: (err as NodeJS.ErrnoException).code })


🚫 REQUEST CHANGES

Summary: The implementation achieves the goal of showing empty folders, with good test coverage for the new buildTree() behavior. The primary concern is the synchronous disk walk on every API call — this will cause noticeable UI stuttering on large content directories and block CRDT operations. Consider either caching the result or maintaining the folder index incrementally (recommended, since the pattern already exists in file-watcher.ts). The symlink handling inconsistency should also be addressed to maintain the documented "full symlink support" contract.

Discarded (3)
Location Issue Reason Discarded
PageListContext.tsx:86-88 Defensive typeof filter on folders array Positive observation — follows existing defensive pattern, no action needed
file-tree-utils.ts:82 Function signature change Positive observation — backward compatible, good use of Iterable<string>
api-extension.ts:2955 New folders field in response Positive observation — follows existing envelope pattern, purely additive
Reviewers (4)
Reviewer Returned Main Findings Consider Inline Comments Discarded
pr-review-standards 1 1 0 1 0
pr-review-consistency 5 1 0 1 3
pr-review-tests 4 0 0 0 4
pr-review-sre 3 1 1 1 0
Total 13 3 1 3 7

Comment on lines +590 to +616
function collectFolderPathsFromDisk(): string[] {
const folders = new Set<string>();

function walk(absDirPath: string, relDirPath: string): void {
let entries: Dirent[];
try {
entries = readdirSync(absDirPath, { withFileTypes: true });
} catch (err) {
console.warn(`[api] Failed to read directory ${absDirPath}:`, err);
return;
}

for (const entry of entries) {
if (!entry.isDirectory()) continue;
const childRelPath = relDirPath ? `${relDirPath}/${entry.name}` : entry.name;
const excludedByFilter = contentFilter?.isDirExcluded(childRelPath) ?? false;
const excludedByFallback = entry.name === '.git' || entry.name === 'node_modules';
if (excludedByFilter || excludedByFallback) continue;

folders.add(childRelPath);
walk(resolve(absDirPath, entry.name), childRelPath);
}
}

walk(contentDir, '');
return [...folders].sort((a, b) => a.localeCompare(b));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 MAJOR: Synchronous disk walk on every /api/pages request

Issue: This function performs a synchronous recursive directory walk using readdirSync() on every GET /api/pages request, blocking the Node.js event loop for the duration of the walk.

Why: The /api/pages endpoint is called frequently: on initial page load, on tab/window focus, on visibility change, and on every CC1 files channel broadcast. For a content directory with hundreds of folders, each call could block for 10-100ms. During this time, all collaborative editing operations (CRDT syncs, WebSocket messages) are paused. The existing getFileIndex() is already async-safe (populated at startup, updated incrementally), but this reintroduces synchronous disk I/O into the hot path.

Fix: Consider maintaining the folder index incrementally alongside the existing fileIndex in file-watcher.ts. The seedLastKnownHashes() function already walks the directory tree at startup — extend it to also track folder paths, then update incrementally on create/delete/rename DiskEvents. Expose via getFolderIndex() similar to getFileIndex().

Alternatively, if this is acceptable for the initial implementation, consider caching the result with a short TTL or invalidating on DiskEvents to reduce the frequency of walks.

Refs:

}

for (const entry of entries) {
if (!entry.isDirectory()) continue;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 MAJOR: Symlinked directories silently excluded

Issue: entry.isDirectory() returns false for symbolic links pointing to directories, causing symlinked folders to be silently excluded from the returned folder list.

Why: CLAUDE.md states "Symlinks inside the content directory are fully supported." The existing file-watcher.ts explicitly handles symlinked directories by first checking isSymbolicLink() then resolving to determine target type (lines 419-460). This new code does not follow that pattern, causing symlinked empty folders to be invisible in the sidebar.

Fix: Check entry.isSymbolicLink() in addition to entry.isDirectory(). For symlinks, resolve with realpathSync and check if the target is a directory, following the pattern in file-watcher.ts:seedLastKnownHashes(). Also handle ENOENT (broken symlink), ELOOP (cyclic symlink), and symlink-escape (target outside contentDir) cases.

Refs:

if (!entry.isDirectory()) continue;
const childRelPath = relDirPath ? `${relDirPath}/${entry.name}` : entry.name;
const excludedByFilter = contentFilter?.isDirExcluded(childRelPath) ?? false;
const excludedByFallback = entry.name === '.git' || entry.name === 'node_modules';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor: Fallback exclusions incomplete compared to BUILTIN_SKIP_DIRS

Issue: The excludedByFallback check hardcodes only .git and node_modules, but BUILTIN_SKIP_DIRS in content-filter.ts (lines 39-62) includes 16 additional directories: .venv, venv, env, __pycache__, vendor, dist, build, out, output, .next, .nuxt, .svelte-kit, .astro, .turbo, .cache, .parcel-cache, and coverage.

Why: When contentFilter is absent (unlikely in production but possible in tests), empty folders like dist/, build/, .next/ would appear in the sidebar. This creates inconsistent behavior between the fallback path and the normal path.

Fix: Either:

  1. Import and use BUILTIN_SKIP_DIRS from content-filter.ts for the fallback
  2. Make contentFilter required since standalone.ts always provides it
  3. Accept the incomplete fallback since it's defensive code for an unlikely case

Refs:

@github-actions github-actions Bot deleted a comment from inkeep-internal-ci Bot Apr 21, 2026
@dimaMachina dimaMachina marked this pull request as draft April 21, 2026 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant