fix(web-ui): deliver batch notifications when execution monitor is unmounted (#652)#691
Conversation
…mounted (#652) Async batch.completed / blocker.created notifications previously only fired while BatchExecutionMonitor was mounted (i.e. on /execution?batch=...). Navigating away meant a background batch finished silently — defeating the purpose of browser notifications. Add a cross-page background watcher (useBatchNotificationWatcher) mounted once inside NotificationProvider (which lives in the root layout, so it runs on every route). It polls the existing GET /api/v2/batches endpoint, establishes a baseline on first poll so already-terminal/blocked batches don't notify, and fires batch.completed on terminal transitions and blocker.created on per-task BLOCKED transitions. An in-flight guard prevents overlapping slow polls from corrupting the transition baseline. Make it the single dispatcher: remove the duplicate dispatch from BatchExecutionMonitor. No backend change — the batches list endpoint already returns per-task results.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughBatch completion and blocker notifications are moved from ChangesCross-page Background Batch Watcher
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
web-ui/src/__tests__/hooks/useBatchNotificationWatcher.test.ts (1)
77-207: ⚡ Quick winAdd a regression test for workspace changes mid-poll.
Current coverage is strong, but it doesn’t assert behavior when
getSelectedWorkspacePath()changes between poll start and response resolution. Add one case that flips workspace before resolvingbatchesApi.listand verifies no stale notification is dispatched.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-ui/src/__tests__/hooks/useBatchNotificationWatcher.test.ts` around lines 77 - 207, Add a new test case within the useBatchNotificationWatcher describe block to verify behavior when the workspace changes between poll start and response resolution. Create a test that mocks a slow list response (similar to the "does not start an overlapping poll" test), starts the renderHook with useBatchNotificationWatcher, calls flushPoll to initiate the pending list call, then changes the workspace by calling mockGetWorkspacePath.mockReturnValue(null) before resolving the slow promise, and finally asserts that addNotification was never called with any notification (ensuring no stale data is dispatched when the workspace becomes invalid mid-poll).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web-ui/src/hooks/useBatchNotificationWatcher.ts`:
- Around line 73-113: The poll callback captures the workspace path at the start
but doesn't verify that the workspace hasn't changed after the async
batchesApi.list call completes. If the selected workspace changes while the
request is in flight, lines 86-110 (including the addRef.current notification
calls and the notifyBlocked call) will emit notifications from the stale
workspace context. Add a workspace validation check immediately after the
try-catch block (similar to the existing cancelled check on line 81) to ensure
the current workspace still matches the workspacePath captured at poll start,
and return early if it has changed.
---
Nitpick comments:
In `@web-ui/src/__tests__/hooks/useBatchNotificationWatcher.test.ts`:
- Around line 77-207: Add a new test case within the useBatchNotificationWatcher
describe block to verify behavior when the workspace changes between poll start
and response resolution. Create a test that mocks a slow list response (similar
to the "does not start an overlapping poll" test), starts the renderHook with
useBatchNotificationWatcher, calls flushPoll to initiate the pending list call,
then changes the workspace by calling mockGetWorkspacePath.mockReturnValue(null)
before resolving the slow promise, and finally asserts that addNotification was
never called with any notification (ensuring no stale data is dispatched when
the workspace becomes invalid mid-poll).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 91fa50d6-2617-4b53-bd5f-cfe1cc30b0e6
📒 Files selected for processing (6)
web-ui/src/__tests__/hooks/useBatchNotificationWatcher.test.tsweb-ui/src/components/execution/BatchExecutionMonitor.tsxweb-ui/src/contexts/NotificationContext.tsxweb-ui/src/hooks/useBatchNotificationWatcher.tsweb-ui/src/lib/api.tsweb-ui/src/types/index.ts
|
Code Review - PR 691: Cross-page batch notifications First review - no prior comments from me on this PR. Overview Solid architecture fix for issue 652. Moving notification dispatch from BatchExecutionMonitor (page-scoped) to useBatchNotificationWatcher (root-layout-scoped) is the right call. The navigate-away blind spot is eliminated cleanly with no backend changes. What is working well
Issues to address 1. notifyBlocked is not guarded by cancelled The call void notifyBlocked(workspacePath, taskId, addRef.current) launches async work not protected by the cancelled flag. If the hook unmounts while a title-fetch is in flight, add still fires. In practice this is harmless because NotificationProvider lives in the root layout and rarely unmounts, but it is a silent invariant violation. Fix: pass () => cancelled and addRef into notifyBlocked instead of the snapshot value, then add if (isCancelled()) return; after the title fetch completes. 2. pollingRef persists across effect re-runs (edge case) pollingRef is defined in the hook body (not inside the effect), so it is shared across effect re-runs triggered by intervalMs changes. If a poll is in-flight when the interval changes, the new effect first poll call sees pollingRef.current === true and skips one tick. Not a correctness bug, but setting pollingRef.current = false in the effect cleanup would make re-triggering cleaner. 3. batchesApi.list silently drops limit: 0 The spread ...(options?.limit ? { limit: options.limit } : {}) silently ignores limit: 0 because 0 is falsy. For the actual call site ({ limit: 50 }) this is fine, but the helper makes a silent promise it does not keep. Prefer options?.limit != null as the guard. Nits
Test suggestions (nice-to-have, not blockers)
Verdict The fix is architecturally correct and the implementation is clean. Items worth addressing before merge: the unguarded void notifyBlocked (issue 1) and the limit guard in api.ts (issue 3, low-effort). Issue 2 is minor. Tests are solid; the two suggested additions are nice-to-have. Approve after issues 1 and 3 are addressed. |
|
Follow-up review — issues from my first review remain open The second commit is CLAUDE.md-only (docs update for the known-limitation note). The hook and API code are unchanged, so all three items from my first review are still outstanding: Still needs fixing before merge
Minor / can defer
Nice-to-have tests (still not blocking):
Nothing new to flag in the diff. The architecture is sound; these are the only items preventing a clean approval. |
|
Addressed CodeRabbit's review in b183c78:
|
|
Follow-up review — after b183c78 Both items I flagged as "needs fixing before merge" in my first review are now resolved or effectively resolved: Issue 1 — The Issue 3 —
Issue 2 — As noted before, this is an edge-case cosmetic concern. Summary: The architecture is sound, the main correctness bug is fixed, test coverage is solid (8 tests). The two remaining nits are genuinely minor and do not affect the shipped behavior. This is ready to merge. |
|
This is the same finding already addressed in b183c78 — the current code matches CodeRabbit's suggested diff exactly: |
Summary
Closes #652.
Async
batch.completed/blocker.creatednotifications previously only fired whileBatchExecutionMonitorwas mounted — i.e. only on/execution?batch=…. Navigate away and a background batch that finishes produces no notification (in-app or browser), defeating the whole point of browser alerts. This was a known limitation called out in CLAUDE.md.This implements the issue's preferred fix: a cross-page background poller.
What changed
useBatchNotificationWatcher(web-ui/src/hooks/useBatchNotificationWatcher.ts): polls the existingGET /api/v2/batchesendpoint on a steady interval (default 10s). It:batch.completedon a batch's transition into a terminal state (COMPLETED/FAILED/CANCELLED) andblocker.createdon a per-task transition into BLOCKED (lazily fetching the task title for a friendly message).NotificationProvider(contexts/NotificationContext.tsx), which lives in the root layout and does not remount on navigation — so the watcher runs on every route and its baseline persists across page changes.batch.completed/blocker.createddispatch fromBatchExecutionMonitorto avoid double notifications on/execution. The monitor keeps its live visual polling/display.batchesApi.list()+BatchListResponsetype. No backend change — the list endpoint already returns per-taskresults.Testing
useBatchNotificationWatcher.test.ts(7 tests): first-poll baseline suppression, terminal transition, fire-once, BLOCKED + title fetch, no-workspace no-op, in-flight overlap guard, unmount cleanup.npm run buildsucceeds; eslint clean on changed files.Known limitations
gate.run.failedon/proofremains page-scoped. Unlike batches, a proof run is a synchronous request/response action the user actively watches on that page — there's no server-tracked background job to poll — so the navigate-away problem doesn't apply there. Left as-is.PARTIALbatch status does not emit abatch.completednotification, matching the pre-existing monitor behavior and the webhook dispatch scope (which intentionally excludes PARTIAL).Summary by CodeRabbit