Skip to content

Pause watch polling automatically while sessions are idle#520

Open
tridha643 wants to merge 4 commits into
mainfrom
codex/add-idle-watcher
Open

Pause watch polling automatically while sessions are idle#520
tridha643 wants to merge 4 commits into
mainfrom
codex/add-idle-watcher

Conversation

@tridha643

@tridha643 tridha643 commented Jul 9, 2026

Copy link
Copy Markdown

Summary

  • Put every --watch session into an automatic low-cost sleep after one minute without keyboard or mouse interaction.
  • Stop scheduling Git-backed watch signature polling while asleep, so unattended Hunk tabs do not keep spawning expensive repository checks.
  • Wake on the next interaction with one immediate refresh, then resume normal polling.
  • Keep wake-up and interval refreshes behind one shared in-flight guard so reactivation cannot start duplicate reloads.
  • Avoid adding a new CLI flag, config key, or environment variable.

Why

It is easy to leave several hunk diff --watch tabs open while agents continue changing a repository. Those unattended sessions previously kept polling Git every 250 ms. Automatic sleep removes that background cost without asking users to configure each tab.

Background diff reloads do not count as user activity, so a busy repository cannot keep an unattended session awake forever.

Testing

  • Added interaction coverage for active to idle to active transitions and the immediate wake-up refresh.
  • Added coverage proving background reloads do not postpone automatic sleep.
  • Kept slow-reload coverage proving wake-up and resumed polling cannot overlap refreshes.
  • bun run typecheck
  • bun run lint
  • bun test ./src ./packages ./scripts ./test/cli ./test/session (1,012 passed, 13 skipped)
  • bun test ./test/pty (51 passed)
  • HUNK_RUN_TTY_SMOKE=1 bun test ./test/smoke (9 passed)

Review guide

  • Start with src/core/watch.ts for the built-in inactivity threshold.
  • Review src/ui/App.tsx for activity tracking, sleep/wake transitions, and refresh coalescing.
  • Review src/ui/AppHost.interactions.test.tsx for the lifecycle and background-reload regressions.

Tradeoff

A passively viewed watch session stops updating after one minute without input. The next key, click, drag, or scroll performs an immediate refresh before normal polling resumes.

@tridha643 tridha643 requested a review from benvinegar July 9, 2026 15:06
@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces automatic idle detection for --watch sessions: after one minute without keyboard or mouse input, polling stops; the next interaction triggers an immediate refresh and resumes polling. A shared in-flight guard (watchRefreshInFlightRef) is used across both the wake-up path and the polling interval to prevent duplicate concurrent refreshes.

  • src/core/watch.ts exports the new WatchSessionActivity type and the 60-second default threshold; App.tsx wires up three useEffects — one that resets state when watch is toggled, one that schedules the idle timeout, and one that runs the polling interval only while active.
  • Activity is captured comprehensively: all keystrokes via useAppKeyboardShortcuts, mouse scroll events via DiffPane.onUserActivity, and drag/click events via App.tsx mouse handlers.
  • Three new integration tests cover the idle→active transition, the requirement that background reloads don't count as user activity, and the overlap-prevention guarantee between wake-up and resumed polling.

Confidence Score: 5/5

Safe to merge. The idle/active state machine is correct, the shared in-flight guard reliably prevents duplicate refreshes across the wake-up and polling paths, and the new integration tests cover the key lifecycle transitions.

The core logic — idle timeout scheduling, guard-based refresh coalescing, and polling lifecycle tied to watchShouldPoll — is sound and well-tested. The only finding is that every keystroke unconditionally increments watchActivityRevision, causing a render and idle-timeout effect re-run even during continuous active use; this is a minor efficiency concern with no correctness impact.

src/ui/App.tsx — the watchActivityRevision increment on every interaction is worth revisiting for render efficiency.

Important Files Changed

Filename Overview
src/ui/App.tsx Adds idle-session tracking via watchActivityRef/watchActivity state pair, a shared in-flight guard (watchRefreshInFlightRef), and three new useEffects for activity reset, idle timeout scheduling, and watch polling. The shared guard correctly prevents duplicate refreshes between wake-up and polling paths. The per-interaction watchActivityRevision increment triggers unnecessary re-renders and effect teardowns on every keystroke/scroll while already active.
src/core/watch.ts Adds WatchSessionActivity type and DEFAULT_WATCH_INACTIVITY_SLEEP_MS constant. Minimal, self-contained, no issues.
src/ui/AppHost.tsx Threads watchInactivitySleepMs prop through from AppHost to App with correct default. No issues.
src/ui/components/panes/DiffPane.tsx Adds optional onUserActivity prop with a safe default no-op; calls it at the start of handleMouseScroll to capture scroll interactions as user activity. Correct dependency array update.
src/ui/hooks/useAppKeyboardShortcuts.ts Adds optional onActivity callback called at the top of the keyboard handler, covering all keystrokes as user activity. Clean integration.
src/ui/AppHost.interactions.test.tsx Adds three new integration tests covering idle→active transition with file change visibility, background reload not postponing sleep, and no duplicate refreshes on wake-up. Tests use small watchInactivitySleepMs values to make timing deterministic.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant U as User
    participant A as App.tsx
    participant T as IdleTimeout (useEffect)
    participant G as watchRefreshInFlightRef
    participant P as Polling (setInterval)
    participant R as refreshCurrentInput

    Note over A,P: Session starts active, polling running
    P->>G: check guard (false)
    P->>R: refreshWatchedInput (file changed)
    G-->>P: "guard=true"
    R-->>G: "finally → guard=false"

    Note over A,T: 60s without input elapses
    T->>A: setWatchSessionActivity("idle")
    A-->>P: "watchShouldPoll=false → interval cleared"

    Note over A,U: File changes while idle — not detected

    U->>A: keypress / click / scroll
    A->>A: markWatchSessionActivity()
    A->>G: check guard (false)
    A->>R: refreshWatchedInput (wake-up, immediate)
    G-->>A: "guard=true"
    A->>A: setWatchActivity("active")
    A->>T: revision++ → new idle countdown
    A-->>P: "watchShouldPoll=true → new interval"

    P->>G: check guard (true, wake-up in flight)
    G-->>P: returns false, skip tick

    R-->>G: "wake-up refresh done → guard=false"
    Note over A,P: bootstrap.input updates → polling effect re-runs with fresh lastSignature
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant U as User
    participant A as App.tsx
    participant T as IdleTimeout (useEffect)
    participant G as watchRefreshInFlightRef
    participant P as Polling (setInterval)
    participant R as refreshCurrentInput

    Note over A,P: Session starts active, polling running
    P->>G: check guard (false)
    P->>R: refreshWatchedInput (file changed)
    G-->>P: "guard=true"
    R-->>G: "finally → guard=false"

    Note over A,T: 60s without input elapses
    T->>A: setWatchSessionActivity("idle")
    A-->>P: "watchShouldPoll=false → interval cleared"

    Note over A,U: File changes while idle — not detected

    U->>A: keypress / click / scroll
    A->>A: markWatchSessionActivity()
    A->>G: check guard (false)
    A->>R: refreshWatchedInput (wake-up, immediate)
    G-->>A: "guard=true"
    A->>A: setWatchActivity("active")
    A->>T: revision++ → new idle countdown
    A-->>P: "watchShouldPoll=true → new interval"

    P->>G: check guard (true, wake-up in flight)
    G-->>P: returns false, skip tick

    R-->>G: "wake-up refresh done → guard=false"
    Note over A,P: bootstrap.input updates → polling effect re-runs with fresh lastSignature
Loading

Reviews (3): Last reviewed commit: "refactor: make watch sleep automatic" | Re-trigger Greptile

Comment thread src/core/watch.ts Outdated
Comment thread src/core/cli.ts
Comment thread src/ui/App.tsx
@tridha643

Copy link
Copy Markdown
Author

@greptile

@tridha643 tridha643 changed the title Add idle timeout for watch refresh polling Pause watch polling automatically while sessions are idle Jul 10, 2026
@tridha643

Copy link
Copy Markdown
Author

@greptile

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