Skip to content

TUI blank page: FSEventStreamSetExclusionPaths blocks 21-35s when HOME has large directories #27082

Closed as not planned

Description

@litown

Environment

  • opencode: v1.14.48
  • OS: macOS 15.4.1 (Apple Silicon arm64)
  • Install method: npm (npm i -g opencode-ai)

Summary

This is a deeper root cause analysis of #27071. The blank TUI is not caused by ENAMETOOLONG errors or by scanning ~/Library content. The true cause is FSEventStreamSetExclusionPaths taking 21-35 seconds to initialize when the exclusion paths (built from os.homedir()) point to directories with millions of files.

Root Cause

Startup Flow

os.homedir() → build exclusion paths → FSEventStreamSetExclusionPaths() → wait → file.watcher.updated → TUI renders

What happens

  1. On startup, opencode builds a list of "protected paths" from os.homedir():
    var home = os.homedir();
    var dirs = ["Music","Pictures","Movies","Downloads","Desktop","Documents","Public","Applications","Library"];
    // + sub-paths under Library: Application Support, Caches, etc.
  2. These paths are passed to FSEventStreamSetExclusionPaths() to exclude them from FSEvents monitoring
  3. FSEventStreamSetExclusionPaths interacts with the volume's FSEvents database to register exclusions
  4. When exclusion paths point to directories with millions of files (e.g., a 150GB ~/Library), this registration takes 21-35 seconds
  5. The TUI blocks on file.watcher.updated event which only fires after FSEvents initialization completes
  6. User sees a blank page for 21-35 seconds

Key Evidence

Test Result
HOME=/minimal/dir (exclusion paths → empty dirs) TUI renders in <1s
HOME=/Users/town (exclusion paths → large dirs) TUI blank for 21-35s
HOME=/Users/town + /tmp clean project dir Still 21-35s delay (not project dir issue)
Ripgrep scan of project dir 14ms for 12 files (not ripgrep bottleneck)
Network blocked (sandbox-exec) Still 21-35s (not network issue)
Fresh data directory Still 21-35s (not data corruption)
Log timing: file.watcher.updated subscribing → publishing Consistent 21-35s gap

The delay is purely caused by os.homedir() returning a HOME with large directories, causing FSEventStreamSetExclusionPaths to be slow.

Why the HOME isolation workaround works

Setting HOME=/path/to/minimal/home makes os.homedir() return that path. The exclusion paths then point to tiny/empty directories, and FSEvents initializes instantly.

Proposed Fixes (in source code)

Fix 1: Don't block TUI on file watcher initialization (Recommended)

Render the TUI immediately instead of waiting for file.watcher.updated:

Current:  startup → FSEvents init (blocks 21-35s) → render TUI
Proposed: startup → render TUI (immediate) → FSEvents init (background)

The file tree can show a loading state and refresh when the watcher is ready. This is a small change with maximum impact.

Fix 2: Only exclude paths under the watched directory

If the file watcher only monitors the project directory (not HOME), there's no need to exclude HOME subdirectories. The exclusion paths should only cover subdirectories of the actual watched path, not all of os.homedir().

Fix 3: Add timeout with fallback

If FSEventStreamSetExclusionPaths takes longer than N seconds, skip waiting and use a fallback (polling or no exclusion).

Workaround (for users)

Create a launcher script that sets HOME to an isolated directory:

#!/bin/bash
export HOME="/path/to/minimal/home"
mkdir -p "$HOME/.config/opencode"
# Copy or link your opencode.json config
exec opencode "$@"

Related

Activity

  1. litown commented on May 14, 2026

    @litown
    Author

    Corrected Root Cause Analysis

    After further source code investigation, the root cause identified above is incorrect. The blank page is not caused by FSEventStreamSetExclusionPaths.

    Actual Root Cause

    The TUI blank page is caused by SyncProvider.bootstrap() in packages/opencode/src/cli/cmd/tui/context/sync.tsx blocking ALL rendering on ALL initial HTTP requests, including app.agents.

    Startup flow (actual):

    TUI starts → SyncProvider.bootstrap() → Promise.allSettled(blockingRequests) → ready=true → render
    

    The blockingRequests array includes:

    1. config.providers ✅ fast
    2. provider.list ✅ fast
    3. app.agents ❌ can hang forever
    4. config.get ✅ fast
    5. project.sync ✅ fast

    Why app.agents hangs: The server-side handler triggers Agent.Service initialization via lazy InstanceState/ScopedCache. If the project directory contains deeply nested node_modules/.pnpm paths, the initialization encounters ENAMETOOLONG errors that are not properly propagated back to the HTTP handler, causing the request to hang indefinitely.

    Why TUI is blank: The ready getter in SyncProvider:

    get ready() {
      if (process.env.OPENCODE_FAST_BOOT) return true
      return store.status !== "loading"
    }

    Combined with createSimpleContext's <Show when={init.ready}>, the entire component tree is blocked until all blocking requests complete.

    Fix

    PR #27448 moves agentsPromise from blockingRequests to the non-blocking section. The TUI renders immediately after essential data loads, while agents load in the background.

    Why the HOME isolation workaround "worked"

    The HOME isolation workaround didn't fix FSEventStreamSetExclusionPaths — it changed the project context so that Agent.Service initialization no longer hit deeply nested paths. The real fix is making agents non-blocking.

    Correction on evidence

    Test Actual explanation
    HOME=/minimal/dir works Different HOME → different project context → Agent.Service init succeeds
    /tmp clean project still hangs Agent.Service still init on original project's node_modules
    Ripgrep is fast File scanning was never the bottleneck — it's the HTTP request hanging
    21-35s file.watcher gap Coincidental timing — the real blocker was the app.agents HTTP request

    I apologize for the initial misdiagnosis. The code-level investigation with the forked source confirmed the actual blocking point.

  2. github-actions commented on Jul 14, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions