fix(web): notify when a new request replaces the pending one in a run#32719

merged6
main
fix/issue-32711
opened2d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 6h ago

Problem

On web and desktop, a second approval or question in the same run gave no sound, toast or desktop notification. This happens when the second request replaces the first before the client sees the thread go back to working, for example two approvals raised by parallel tool calls. The notification key was only ${runId}:${status} and a notification fires only when that key changes, so the second request matched the first key and the agent sat blocked with only a sidebar badge.

Reproduction: the coordinator's browser test drives the real startThreadNotifications through the shell atom. It moves a thread from pending request request-1 to pending request request-2 of the same kind in the same run. On main it sends one notification instead of two, for both approvals and questions.

Change

The attention key in ThreadNotificationCoordinator.tsx now also carries the id of the request behind the status. The id comes from the existing resolveBackgroundThreadRequest, which the background screen-reader announcer already uses for the same comparison. A thread's own approval or question contributes its id. Failed, usage-limit and guest-request statuses take no id, so a pending auth refresh does not change their key.

With ids in the key, answering the newer of two pending requests would bring the older one back and announce it again. The notification gate (ThreadNotificationCoordinator.logic.ts) therefore remembers the request keys a thread has shown since it last stopped needing attention, and notifies each one once. The memory clears when the thread goes back to working or settles.

A subagent's request reaches the parent with no id, so it keeps the previous rule: it notifies whenever the key changes. A second subagent request after the parent's own request still notifies. The cost is that a subagent request that becomes the pending one again after the parent's own request is answered can notify a second time.

Scope and approval

Fixes pingdotgg/t3code#32711

Verification

Results on head ce7b5659a3 (main merged in):

  • apps/web: vp test run --project browser src/components/ThreadNotificationCoordinator.badge.browser.test.ts: 9 pass. On test-only commit 7f4311dac3 (main plus the new test), both new cases fail: 2 failed and 7 passed.
  • apps/web: vp test run src/components/ThreadNotificationCoordinator.logic.test.ts: 6 pass. Its gate case now covers request A, then B, then A again (two notifications; three before the gate change), and own question, then id-less subagent approval, then the question again, then a second id-less subagent approval (it notifies; it was silent before the subagent rule).
  • vp run check:changed: format, lint, knip, typecheck, tests and react compiler pass.

Transcript excerpt from the original red and green runs:

1== red: test-only commit (main a9e709f15f + test) ==
2 × notifies again when a new hasPendingApprovals request replaces the pending one in the same run
3 × notifies again when a new hasPendingUserInput request replaces the pending one in the same run
4 Tests 2 failed | 7 passed (9)
5== green: fix head ==
6 Tests 9 passed (9)

Evidence

Captured A1 and J: 2 / 19 matrix rows; 2 / 2 required rows complete for #32719. One before/after recording pair covers both rows. Shell-driver harness: the real coordinator and normal app toast renderer receive synthetic shell atoms. This demonstrates client notification behavior, not server-generated concurrency.

The identical script initializes working run R, shows approval A at sequence 00:02, and replaces it with approval B at 00:04, keeping run R and approval status unchanged with no working gap. At 00:05, hover exposes and pauses the toast stack. Measured from the recordings at 30 fps: the working state first shows at video 00:02.07 before / 00:01.87 after, approval A at 00:04.00 / 00:03.83, and approval B at 00:06.03 / 00:05.87. Each toast slides in from the right edge over about half a second (the toast's own entrance transition, unchanged by this PR), so frames during entry show it partly offscreen. The on-screen labels identify each event. Before: one toast. After: two toasts.

FixtureBeforeAfterDiff0DiffT
A1 / J · 1280 × 800 · darkbefore.png
f12bb6229c
after.png
12d0972ba0
diff0.png
f12bb6229c → 12d0972ba0
diff5pct.png
f12bb6229c → 12d0972ba0

Before recording — f12bb6229c: before.mp4

After recording — 12d0972ba0: after.mp4

Environment: T3 Browser panel, Chromium 157 on Linux x86_64; no device emulation; 1280 × 800 CSS viewport; DPR 2; zoom 1; root font 16 px / 100% font scaling; dark theme; scroll (0,0); actual document visible and focused. Date and Date.now frozen at 2026-09-13 07:00:00 UTC; timers/animation clocks continue. Both sides run their own checkout through T3CODE_DEV_SEED=0 vp run dev. The same one-thread synthetic seed, controlled other-thread route, settings and driver are used. In-app notifications enabled; sound and OS notifications disabled. No notification permission grant/stub was needed. Original DPR-2 assets are retained in the source archive; published stills/videos are downsampled to 1280 × 800 with no cropping or compositing.

Settled-frame validation: On each side, take a screenshot, wait 500 ms, then take another; browser-tool overhead adds roughly 0.4 s. ImageMagick absolute-error count is 0 for both full-resolution pairs. Both images, their stability checks, both diff images and the recording filmstrips were opened and inspected before upload. Diff0 is the absolute per-channel difference amplified at threshold 0; DiffT uses 5% of the channel range (12.75 / 255). The difference bounds are x=864, y=155, width=408, height=121 at publication resolution.

What else changed:

  • Only the additional toast, its border, dismiss/action controls and shadow differ. Every pixel outside that region is identical; the first toast and harness content match.
  • Video lengths differ by about 0.33 seconds from capture-tool overhead (before 8.50 s; after 8.83 s); the scripted input timing is identical.

Drivers and reproducibility:

To rebuild: select dark appearance explicitly (setAppearanceMode('dark')) before running the driver; setTheme('dark') alone keeps system-following appearance.

shell-driver.js

process-evidence.py

validation.json

source-captures-and-drivers.zip

The archive retains the final raw recordings, both original settled pairs and every driver version written during capture. No source or evidence was committed. The after checkout is clean; the temporary before checkout was removed; all four test ports and the owned browser tab were closed.

Unverified: No required capture remains missing. A2, B–I and K–R were not separately captured or independently rerun; their existing test/source classifications remain as stated in the supplied fixture matrix. Native OS banners/badges, audible playback, desktop-native/mobile behavior and actual server concurrency are not established by this pair. Current-head automated tests/base-red execution were outside this evidence-capture brief and are not claimed here.

Not checked:

  • Native OS notification presentation and sound in a real desktop client. The tests assert the Notification constructed by the coordinator.
  • Two overlapping requests raised by a real provider. The recording drives the shell state directly.
  • Mobile push notifications, which come from the server relay rather than this coordinator.

Review: three rounds by two reviewers. Round 1 found the older request being announced again, and round 2 found a second id-less subagent request going silent; both are fixed above. Round 3: no findings.

AI-assisted; verified with the tests and tools listed above.

Checks

22 successful · 23 skipped
All checks passed
ce7b565

Added 2 commits

  • fix(web): notify when a new request replaces the pending one in a run3f468c4
  • test(web): reproduce #32711, a replacing pending request does not notify again7f4311d
Synced from GitHub

Approvability

Verdict: Approved at 12d0972

Macroscope's review found this PR approvable — Replacement pending requests can now trigger a fresh notification within the same run, while repeated request identities are deduplicated. Existing notification preferences and delivery safeguards remain unchanged.

No code changes detected at ce7b565. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The coordinator now includes the resolved background request ID in attention state. The notification gate tracks attended attention keys per thread. Tests cover repeated attention observations and changed request IDs for approval and user-input requests.

Changes: Thread notifications

Layer / File(s)Summary
Request-aware notification gate
apps/web/src/components/ThreadNotificationCoordinator.logic.ts, apps/web/src/components/ThreadNotificationCoordinator.logic.test.ts
Observations include a nullable request ID. The gate tracks attended attention keys, uses them to determine input notification freshness, and clears their history when attention ends or threads are removed. Tests cover changing and repeated attention observations.
Coordinator request IDs and browser coverage
apps/web/src/components/ThreadNotificationCoordinator.tsx, apps/web/src/components/ThreadNotificationCoordinator.badge.browser.test.ts
For attention-needing statuses, the coordinator includes the resolved background request ID in the attention key and notification state. Browser tests verify that a changed request ID produces a second notification for approval and user-input requests in the same run.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Possibly related PRs

  • pingdotgg/t3code#25866: Changes attention deduplication for pending input and approval notifications using status-based keys.
  • pingdotgg/t3code#29189: Changes the Windows notification presentation path in the coordinator that delivers these notifications.

Suggested reviewers: t3dotgg, yash-singh1

Merge Risk: External image Minimal · up to 12d09

The request-aware notification change is ready to merge after normal checks; no actionable merge-blocking risk remains.

Pre-merge checks: Passed 4

Check nameStatusExplanation
Title checkPassedThe title clearly and concisely describes the main change: notifying when a new request replaces a pending request in the same run.
Linked Issues checkPassedIssue #32711 requires a notification for each new pending approval or question in the same run, including when the client does not observe a working state between requests. ThreadNotificationCoordinat…
Out of Scope Changes checkPassedAll changes support issue #32711. The coordinator change adds request identity to attention state. The gate change prevents duplicate notifications when an already-seen request key returns. The browse…
Description checkPassedThe description covers the problem, change, linked issue, and focused verification in detail. It includes before-and-after UI evidence and states what remains unverified. It says AI assistance was use…
Full details: Linked Issues check

Explanation

Issue #32711 requires a notification for each new pending approval or question in the same run, including when the client does not observe a working state between requests. ThreadNotificationCoordinator.tsx now adds the resolved request ID to the attention key. ThreadNotificationCoordinator.logic.ts tracks attended request keys until attention ends, so a new ID notifies while returning to an already-seen key does not. The browser regression test covers replacement requests for approvals and user input. The logic tests cover repeated keys, returning to an earlier key, attention ending, and ID-less subagent requests.

Full details: Out of Scope Changes check

Explanation

All changes support issue #32711. The coordinator change adds request identity to attention state. The gate change prevents duplicate notifications when an already-seen request key returns. The browser and logic tests cover these behaviors. No unrelated change appears in the whole-PR diff.

Full details: Description check

Explanation

The description covers the problem, change, linked issue, and focused verification in detail. It includes before-and-after UI evidence and states what remains unverified. It says AI assistance was used but does not name the model and harness as the template requests.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
External image Recent review info

External image Run configuration

External image Commits

Reviewing files that changed from the base of the PR and between 3f468c4669bf736b58d70ad31e0be4b9dedc73db and 12d0972ba02040e9760d960e141adedc023c4dde.

External image Files selected for processing: 3

  • apps/web/src/components/ThreadNotificationCoordinator.logic.test.ts
  • apps/web/src/components/ThreadNotificationCoordinator.logic.ts
  • apps/web/src/components/ThreadNotificationCoordinator.tsx

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

Added 3 commits

  • Merge remote-tracking branch 'origin/main' into pr-327197108d5a
  • fix(web): do not re-announce a request already notified in the same stretch1f1e60e
  • fix(web): keep notifying id-less subagent requests12d0972

Added 1 commit

  • Merge remote-tracking branch 'origin/main' into pr-32719ce7b565