Skip to content

fix(penpal): eliminate battery drain from filesystem watching#358

Merged
loganj merged 1 commit into
mainfrom
loganj/focus-watcher
Mar 8, 2026
Merged

fix(penpal): eliminate battery drain from filesystem watching#358
loganj merged 1 commit into
mainfrom
loganj/focus-watcher

Conversation

@loganj
Copy link
Copy Markdown
Collaborator

@loganj loganj commented Mar 8, 2026

Motivation

Penpal was draining battery even when idle, showing up in macOS "Using Significant Energy" warnings. Investigation revealed the watcher was deep-watching all sources and comments directories for every discovered project at startup — ~16K file descriptors across 114 projects — causing constant CPU activity from filesystem events and directory walks.

Approach

Only deep-watch what the current tab actually needs:

  • FilePage: watch the single directory containing the viewed file
  • ProjectPage: watch the project's source and comment directories
  • WorkspacePage / Recent / InReview / Search: no deep watches (workspace roots are already watched for project discovery)

The frontend calls POST /api/focus?project=X (project-level) or POST /api/focus?project=X&path=Y (file-level) on navigation, and DELETE /api/focus when moving to pages that don't need deep watching.

Test plan

  • Watcher tests: FocusProject, FocusFile, focus switching/cleanup, ClearFocus (5 tests)
  • Server tests: focus endpoint with project, file, clear, and missing param (4 tests)
  • Frontend tests: all 148 tests pass with updated mocks
  • Manual: confirmed "Using Significant Energy" badge clears after installing new build

🤖 Generated with Claude Code

Previously the watcher deep-watched all sources and comments directories
for every discovered project at startup (~16K file descriptors across
114 projects), causing constant CPU activity from filesystem events and
directory walks that drained battery even when idle.

Now the watcher only deep-watches what the current tab needs:
- FilePage: the single directory containing the viewed file
- ProjectPage: the project's source and comment directories
- WorkspacePage/Recent/InReview/Search: no deep watches (workspace
  roots are already watched for project discovery)

The frontend calls POST /api/focus?project=X (project-level) or
POST /api/focus?project=X&path=Y (file-level) on navigation, and
DELETE /api/focus when moving to pages that don't need it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@loganj loganj changed the title fix(penpal): only watch filesystem paths relevant to the focused tab fix(penpal): eliminate battery drain from filesystem watching Mar 8, 2026
@loganj loganj merged commit fbfdf97 into main Mar 8, 2026
3 of 4 checks passed
@loganj loganj deleted the loganj/focus-watcher branch March 8, 2026 16:22
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab8a9e9812

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 112 to +114
for _, p := range projects {
w.watchProject(p)
if err := w.watcher.Add(p.Path); err != nil {
log.Printf("Warning: could not watch project root %s: %v", p.Path, err)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Rebuild focused watches after project refresh

After refreshAfterConfigChange rescans projects, Watcher.Refresh now only re-adds project roots and never reapplies the current focus watch set. If a user is already on a ProjectPage and adds/removes sources or worktrees, focusWatched remains stale, so filesystem changes under newly discovered source directories no longer emit files events until focus is changed away and back.

Useful? React with 👍 / 👎.

Comment on lines 374 to 377
w.cache.RescanWith(projects)
for _, proj := range projects {
w.watchProject(proj)
w.watcher.Add(proj.Path)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Re-apply project focus after auto-detected source rescan

When an auto-detect source directory (for example thoughts/) is created under a project root, this rescan path updates the cache but only adds shallow root watches. If that project is currently focused, the new source tree is never added to focusWatched, so subsequent edits inside that directory are missed until another explicit FocusProject call occurs.

Useful? React with 👍 / 👎.

matt2e added a commit that referenced this pull request Jun 4, 2026
…ommands

Restore BOT-686 / goose-internal PR #358 parity that the better-doctor
migration dropped: direct npmjs.org access is blocked by Cloudflare
WARP, so npm install/view must be routed through Block's internal
Artifactory proxy via --registry=<url>. This wires an optional,
caller-supplied registry through the crate without baking in any
Block-specific URL.

- Add `npm_registry: Option<String>` to RunChecksOptions (default None).
- Add `apply_npm_registry(command, registry)` helper that appends
  ` --registry=<url>` only to npm-backed commands (npm install / npm
  view); curl-pipe installers, auth commands, and the git-clonefile fix
  pass through unchanged.
- Apply the registry to the displayed fix_command: threaded through
  run_checks_with_options -> collect_base_report -> check_single_ai_agent
  so the shown command matches what runs.
- Apply the registry to the freshness probe: latest_npm now adds
  --registry <url>, threaded via populate_freshness -> fetch_version_info.
- Apply the registry to the execute path: new
  execute_fix_with_options / execute_fix_streaming_with_options resolve
  the command, run it through apply_npm_registry, then shell out. The
  existing execute_fix / execute_fix_streaming delegate with None.

Purely additive: every new field/param is Option-defaulting-None, so
RunChecksOptions::default() reproduces today's exact commands and the
staged Tauri app compiles and behaves unchanged. The caller
(goose-internal) supplies the Block Artifactory URL.

Not in this commit (follow-ups): per-OS/platform install recipes (amp's
CLI installer) and per-package name overrides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
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