feat: notify when a chat needs input (desktop + web) - #14

Merged
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications
Jul 26, 2026
Merged

feat: notify when a chat needs input (desktop + web)#14
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#11

What

Fires a notification when a chat transitions into a state that needs you — approval requested, agent question (waiting for input), or turn finished — on both surfaces:

  • Desktop (Electron): native OS notification from the main process. Clicking it reveals/focuses the window and navigates to the chat.
  • Web (desktop or mobile browser): Web Notifications API from the client (works for background tabs, e.g. phone browser over Tailscale). Clicking focuses the tab and navigates to the chat.

How

  • Detection rides the existing projectThreadAwareness() phase ladder (@t3tools/shared/agentAwareness) — the same source of truth the mobile push pipeline uses. A pure edge-detecting tracker (apps/web/src/notifications/needsAttention.logic.ts) diffs phases across thread-upserted shell updates: fires on transitions into waiting_for_approval / waiting_for_input, and on running → completed only (which also kills the spurious "Done" at thread birth). First observation of a thread never fires, so reloading with an already-waiting thread stays silent.
  • One coordinator for both surfaces: Electron renders the same web bundle, so a single <NotificationCoordinator/> in the root route watches all threads across all connected environments and dispatches through a platform shim — desktopBridge.showNotification(...) when present, else new Notification(...) (guarded: Android Chrome's page-scope constructor throw is swallowed).
  • Desktop IPC: new desktop:show-notification / desktop:notification-activated channels, an ElectronNotification Effect service (coalesces per-thread via the request id, retains live references), preload bridge members typed in @t3tools/contracts (optional, so older shells hosting newer bundles degrade to the web path).
  • Suppression: no notification when the tab/window is focused and that exact chat is on screen (including the draft route right after promotion). Different chat on screen → still notifies.
  • Settings:notifyOnNeedsInput client setting (default on), row in Settings → General ("Notify when an agent needs input"). On web, enabling requests browser permission from the toggle gesture; a denied permission shows an inline hint; a once-per-session toast nudges permission when a notification would have fired but permission was never asked.

Out of scope (per issue): Web Push for closed tabs (needs service worker + push infra; iOS Safari additionally requires an installed PWA).

Testing

  • Unit: 40+ new tests across the tracker (every behavior rule + mutation-tested edges), the notifier shim (platform routing, constructor-throw guard), coordinator pure helpers (input building/dedupe, activation params), permission-prompt truth table, contracts decode defaults, and the Electron service/IPC method (fake-Electron harness: click→reveal→broadcast, coalescing, unsupported guard). Full suites green: web 172 files / 1505 tests, desktop 53 / 377, contracts 202. Typecheck + lint clean on all touched packages.
  • Live end-to-end (real browser, real Claude CLI turns, isolated --home-dir instance): turn-completion and agent-question notifications fire exactly once with correct title/body/tag while the tab is genuinely blurred; startup grace on reload; suppression when viewing the firing chat (and the converse — other-route transitions do notify); settings toggle gates and persists; notification click navigates to the right thread. Re-smoked after the final fix wave.
  • Desktop:pnpm build:desktop + pnpm test:desktop-smoke pass. (Native notification click path is unit-tested via fakes; not OS-driven e2e.)

Review process

Built task-by-task with per-task spec+quality reviews, a final whole-branch review, and a verified fix wave (notably: Android Chrome constructor guard, desktop Notification Center coalescing, draft-route suppression). Known accepted edges: notifications for since-deleted threads navigate to a dead route; a macOS click with zero open windows doesn't recreate one; permission state in the settings panel refreshes on interaction, not on browser-chrome changes.

🤖 Generated with Claude Code

radroidand others added 6 commits July 25, 2026 09:53
…ification surface
Adds the `notifyOnNeedsInput` client setting (default on, opt-out) to
`ClientSettingsSchema` and `ClientSettingsPatch`, plus the renderer-facing
`DesktopNotificationRequest` / `DesktopNotificationActivation` types and the
optional `showNotification` / `onNotificationActivated` members on
`DesktopBridge`. Both bridge members are optional like `preview` so a newer
web bundle can run inside an older desktop shell and feature-detect.
The desktop `ClientSettings` test fixture gains the new required field so the
repo still typechecks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…avigate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…top coalescing, draft-route suppression
Guard `new Notification(...)` against Android Chrome's illegal-constructor
throw, coalesce desktop notifications by request id, resolve the active
thread through the draft route, hide the denied hint while the toggle is
off, and pin the shim's platform routing with tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 414baf24-9726-445c-833e-17e65b3ea373

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/chat-input-notifications

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit 761b46c into mainJul 26, 2026
1 check passed
radroid added a commit that referenced this pull request Jul 27, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Jul 28, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroid deleted the t3code/chat-input-notifications branch July 30, 2026 14:23
github-actionsBot pushed a commit that referenced this pull request Aug 2, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 3, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto 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.

Notify the user when a chat needs input (desktop + web/mobile browser)

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: notify when a chat needs input (desktop + web) - #14

Merged
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications
Jul 26, 2026
Merged

feat: notify when a chat needs input (desktop + web)#14
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#11

What

Fires a notification when a chat transitions into a state that needs you — approval requested, agent question (waiting for input), or turn finished — on both surfaces:

  • Desktop (Electron): native OS notification from the main process. Clicking it reveals/focuses the window and navigates to the chat.
  • Web (desktop or mobile browser): Web Notifications API from the client (works for background tabs, e.g. phone browser over Tailscale). Clicking focuses the tab and navigates to the chat.

How

  • Detection rides the existing projectThreadAwareness() phase ladder (@t3tools/shared/agentAwareness) — the same source of truth the mobile push pipeline uses. A pure edge-detecting tracker (apps/web/src/notifications/needsAttention.logic.ts) diffs phases across thread-upserted shell updates: fires on transitions into waiting_for_approval / waiting_for_input, and on running → completed only (which also kills the spurious "Done" at thread birth). First observation of a thread never fires, so reloading with an already-waiting thread stays silent.
  • One coordinator for both surfaces: Electron renders the same web bundle, so a single <NotificationCoordinator/> in the root route watches all threads across all connected environments and dispatches through a platform shim — desktopBridge.showNotification(...) when present, else new Notification(...) (guarded: Android Chrome's page-scope constructor throw is swallowed).
  • Desktop IPC: new desktop:show-notification / desktop:notification-activated channels, an ElectronNotification Effect service (coalesces per-thread via the request id, retains live references), preload bridge members typed in @t3tools/contracts (optional, so older shells hosting newer bundles degrade to the web path).
  • Suppression: no notification when the tab/window is focused and that exact chat is on screen (including the draft route right after promotion). Different chat on screen → still notifies.
  • Settings:notifyOnNeedsInput client setting (default on), row in Settings → General ("Notify when an agent needs input"). On web, enabling requests browser permission from the toggle gesture; a denied permission shows an inline hint; a once-per-session toast nudges permission when a notification would have fired but permission was never asked.

Out of scope (per issue): Web Push for closed tabs (needs service worker + push infra; iOS Safari additionally requires an installed PWA).

Testing

  • Unit: 40+ new tests across the tracker (every behavior rule + mutation-tested edges), the notifier shim (platform routing, constructor-throw guard), coordinator pure helpers (input building/dedupe, activation params), permission-prompt truth table, contracts decode defaults, and the Electron service/IPC method (fake-Electron harness: click→reveal→broadcast, coalescing, unsupported guard). Full suites green: web 172 files / 1505 tests, desktop 53 / 377, contracts 202. Typecheck + lint clean on all touched packages.
  • Live end-to-end (real browser, real Claude CLI turns, isolated --home-dir instance): turn-completion and agent-question notifications fire exactly once with correct title/body/tag while the tab is genuinely blurred; startup grace on reload; suppression when viewing the firing chat (and the converse — other-route transitions do notify); settings toggle gates and persists; notification click navigates to the right thread. Re-smoked after the final fix wave.
  • Desktop:pnpm build:desktop + pnpm test:desktop-smoke pass. (Native notification click path is unit-tested via fakes; not OS-driven e2e.)

Review process

Built task-by-task with per-task spec+quality reviews, a final whole-branch review, and a verified fix wave (notably: Android Chrome constructor guard, desktop Notification Center coalescing, draft-route suppression). Known accepted edges: notifications for since-deleted threads navigate to a dead route; a macOS click with zero open windows doesn't recreate one; permission state in the settings panel refreshes on interaction, not on browser-chrome changes.

🤖 Generated with Claude Code

radroidand others added 6 commits July 25, 2026 09:53
…ification surface
Adds the `notifyOnNeedsInput` client setting (default on, opt-out) to
`ClientSettingsSchema` and `ClientSettingsPatch`, plus the renderer-facing
`DesktopNotificationRequest` / `DesktopNotificationActivation` types and the
optional `showNotification` / `onNotificationActivated` members on
`DesktopBridge`. Both bridge members are optional like `preview` so a newer
web bundle can run inside an older desktop shell and feature-detect.
The desktop `ClientSettings` test fixture gains the new required field so the
repo still typechecks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…avigate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…top coalescing, draft-route suppression
Guard `new Notification(...)` against Android Chrome's illegal-constructor
throw, coalesce desktop notifications by request id, resolve the active
thread through the draft route, hide the denied hint while the toggle is
off, and pin the shim's platform routing with tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 414baf24-9726-445c-833e-17e65b3ea373

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/chat-input-notifications

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit 761b46c into mainJul 26, 2026
1 check passed
radroid added a commit that referenced this pull request Jul 27, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Jul 28, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroid deleted the t3code/chat-input-notifications branch July 30, 2026 14:23
github-actionsBot pushed a commit that referenced this pull request Aug 2, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 3, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto 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.

Notify the user when a chat needs input (desktop + web/mobile browser)

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: notify when a chat needs input (desktop + web) - #14

Merged
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications
Jul 26, 2026
Merged

feat: notify when a chat needs input (desktop + web)#14
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#11

What

Fires a notification when a chat transitions into a state that needs you — approval requested, agent question (waiting for input), or turn finished — on both surfaces:

  • Desktop (Electron): native OS notification from the main process. Clicking it reveals/focuses the window and navigates to the chat.
  • Web (desktop or mobile browser): Web Notifications API from the client (works for background tabs, e.g. phone browser over Tailscale). Clicking focuses the tab and navigates to the chat.

How

  • Detection rides the existing projectThreadAwareness() phase ladder (@t3tools/shared/agentAwareness) — the same source of truth the mobile push pipeline uses. A pure edge-detecting tracker (apps/web/src/notifications/needsAttention.logic.ts) diffs phases across thread-upserted shell updates: fires on transitions into waiting_for_approval / waiting_for_input, and on running → completed only (which also kills the spurious "Done" at thread birth). First observation of a thread never fires, so reloading with an already-waiting thread stays silent.
  • One coordinator for both surfaces: Electron renders the same web bundle, so a single <NotificationCoordinator/> in the root route watches all threads across all connected environments and dispatches through a platform shim — desktopBridge.showNotification(...) when present, else new Notification(...) (guarded: Android Chrome's page-scope constructor throw is swallowed).
  • Desktop IPC: new desktop:show-notification / desktop:notification-activated channels, an ElectronNotification Effect service (coalesces per-thread via the request id, retains live references), preload bridge members typed in @t3tools/contracts (optional, so older shells hosting newer bundles degrade to the web path).
  • Suppression: no notification when the tab/window is focused and that exact chat is on screen (including the draft route right after promotion). Different chat on screen → still notifies.
  • Settings:notifyOnNeedsInput client setting (default on), row in Settings → General ("Notify when an agent needs input"). On web, enabling requests browser permission from the toggle gesture; a denied permission shows an inline hint; a once-per-session toast nudges permission when a notification would have fired but permission was never asked.

Out of scope (per issue): Web Push for closed tabs (needs service worker + push infra; iOS Safari additionally requires an installed PWA).

Testing

  • Unit: 40+ new tests across the tracker (every behavior rule + mutation-tested edges), the notifier shim (platform routing, constructor-throw guard), coordinator pure helpers (input building/dedupe, activation params), permission-prompt truth table, contracts decode defaults, and the Electron service/IPC method (fake-Electron harness: click→reveal→broadcast, coalescing, unsupported guard). Full suites green: web 172 files / 1505 tests, desktop 53 / 377, contracts 202. Typecheck + lint clean on all touched packages.
  • Live end-to-end (real browser, real Claude CLI turns, isolated --home-dir instance): turn-completion and agent-question notifications fire exactly once with correct title/body/tag while the tab is genuinely blurred; startup grace on reload; suppression when viewing the firing chat (and the converse — other-route transitions do notify); settings toggle gates and persists; notification click navigates to the right thread. Re-smoked after the final fix wave.
  • Desktop:pnpm build:desktop + pnpm test:desktop-smoke pass. (Native notification click path is unit-tested via fakes; not OS-driven e2e.)

Review process

Built task-by-task with per-task spec+quality reviews, a final whole-branch review, and a verified fix wave (notably: Android Chrome constructor guard, desktop Notification Center coalescing, draft-route suppression). Known accepted edges: notifications for since-deleted threads navigate to a dead route; a macOS click with zero open windows doesn't recreate one; permission state in the settings panel refreshes on interaction, not on browser-chrome changes.

🤖 Generated with Claude Code

radroidand others added 6 commits July 25, 2026 09:53
…ification surface
Adds the `notifyOnNeedsInput` client setting (default on, opt-out) to
`ClientSettingsSchema` and `ClientSettingsPatch`, plus the renderer-facing
`DesktopNotificationRequest` / `DesktopNotificationActivation` types and the
optional `showNotification` / `onNotificationActivated` members on
`DesktopBridge`. Both bridge members are optional like `preview` so a newer
web bundle can run inside an older desktop shell and feature-detect.
The desktop `ClientSettings` test fixture gains the new required field so the
repo still typechecks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…avigate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…top coalescing, draft-route suppression
Guard `new Notification(...)` against Android Chrome's illegal-constructor
throw, coalesce desktop notifications by request id, resolve the active
thread through the draft route, hide the denied hint while the toggle is
off, and pin the shim's platform routing with tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 414baf24-9726-445c-833e-17e65b3ea373

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/chat-input-notifications

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit 761b46c into mainJul 26, 2026
1 check passed
radroid added a commit that referenced this pull request Jul 27, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Jul 28, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroid deleted the t3code/chat-input-notifications branch July 30, 2026 14:23
github-actionsBot pushed a commit that referenced this pull request Aug 2, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 3, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto 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.

Notify the user when a chat needs input (desktop + web/mobile browser)

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: notify when a chat needs input (desktop + web) - #14

Merged
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications
Jul 26, 2026
Merged

feat: notify when a chat needs input (desktop + web)#14
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#11

What

Fires a notification when a chat transitions into a state that needs you — approval requested, agent question (waiting for input), or turn finished — on both surfaces:

  • Desktop (Electron): native OS notification from the main process. Clicking it reveals/focuses the window and navigates to the chat.
  • Web (desktop or mobile browser): Web Notifications API from the client (works for background tabs, e.g. phone browser over Tailscale). Clicking focuses the tab and navigates to the chat.

How

  • Detection rides the existing projectThreadAwareness() phase ladder (@t3tools/shared/agentAwareness) — the same source of truth the mobile push pipeline uses. A pure edge-detecting tracker (apps/web/src/notifications/needsAttention.logic.ts) diffs phases across thread-upserted shell updates: fires on transitions into waiting_for_approval / waiting_for_input, and on running → completed only (which also kills the spurious "Done" at thread birth). First observation of a thread never fires, so reloading with an already-waiting thread stays silent.
  • One coordinator for both surfaces: Electron renders the same web bundle, so a single <NotificationCoordinator/> in the root route watches all threads across all connected environments and dispatches through a platform shim — desktopBridge.showNotification(...) when present, else new Notification(...) (guarded: Android Chrome's page-scope constructor throw is swallowed).
  • Desktop IPC: new desktop:show-notification / desktop:notification-activated channels, an ElectronNotification Effect service (coalesces per-thread via the request id, retains live references), preload bridge members typed in @t3tools/contracts (optional, so older shells hosting newer bundles degrade to the web path).
  • Suppression: no notification when the tab/window is focused and that exact chat is on screen (including the draft route right after promotion). Different chat on screen → still notifies.
  • Settings:notifyOnNeedsInput client setting (default on), row in Settings → General ("Notify when an agent needs input"). On web, enabling requests browser permission from the toggle gesture; a denied permission shows an inline hint; a once-per-session toast nudges permission when a notification would have fired but permission was never asked.

Out of scope (per issue): Web Push for closed tabs (needs service worker + push infra; iOS Safari additionally requires an installed PWA).

Testing

  • Unit: 40+ new tests across the tracker (every behavior rule + mutation-tested edges), the notifier shim (platform routing, constructor-throw guard), coordinator pure helpers (input building/dedupe, activation params), permission-prompt truth table, contracts decode defaults, and the Electron service/IPC method (fake-Electron harness: click→reveal→broadcast, coalescing, unsupported guard). Full suites green: web 172 files / 1505 tests, desktop 53 / 377, contracts 202. Typecheck + lint clean on all touched packages.
  • Live end-to-end (real browser, real Claude CLI turns, isolated --home-dir instance): turn-completion and agent-question notifications fire exactly once with correct title/body/tag while the tab is genuinely blurred; startup grace on reload; suppression when viewing the firing chat (and the converse — other-route transitions do notify); settings toggle gates and persists; notification click navigates to the right thread. Re-smoked after the final fix wave.
  • Desktop:pnpm build:desktop + pnpm test:desktop-smoke pass. (Native notification click path is unit-tested via fakes; not OS-driven e2e.)

Review process

Built task-by-task with per-task spec+quality reviews, a final whole-branch review, and a verified fix wave (notably: Android Chrome constructor guard, desktop Notification Center coalescing, draft-route suppression). Known accepted edges: notifications for since-deleted threads navigate to a dead route; a macOS click with zero open windows doesn't recreate one; permission state in the settings panel refreshes on interaction, not on browser-chrome changes.

🤖 Generated with Claude Code

radroidand others added 6 commits July 25, 2026 09:53
…ification surface
Adds the `notifyOnNeedsInput` client setting (default on, opt-out) to
`ClientSettingsSchema` and `ClientSettingsPatch`, plus the renderer-facing
`DesktopNotificationRequest` / `DesktopNotificationActivation` types and the
optional `showNotification` / `onNotificationActivated` members on
`DesktopBridge`. Both bridge members are optional like `preview` so a newer
web bundle can run inside an older desktop shell and feature-detect.
The desktop `ClientSettings` test fixture gains the new required field so the
repo still typechecks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…avigate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…top coalescing, draft-route suppression
Guard `new Notification(...)` against Android Chrome's illegal-constructor
throw, coalesce desktop notifications by request id, resolve the active
thread through the draft route, hide the denied hint while the toggle is
off, and pin the shim's platform routing with tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 414baf24-9726-445c-833e-17e65b3ea373

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/chat-input-notifications

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit 761b46c into mainJul 26, 2026
1 check passed
radroid added a commit that referenced this pull request Jul 27, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Jul 28, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroid deleted the t3code/chat-input-notifications branch July 30, 2026 14:23
github-actionsBot pushed a commit that referenced this pull request Aug 2, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 3, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto 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.

Notify the user when a chat needs input (desktop + web/mobile browser)

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: notify when a chat needs input (desktop + web) - #14

Merged
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications
Jul 26, 2026
Merged

feat: notify when a chat needs input (desktop + web)#14
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#11

What

Fires a notification when a chat transitions into a state that needs you — approval requested, agent question (waiting for input), or turn finished — on both surfaces:

  • Desktop (Electron): native OS notification from the main process. Clicking it reveals/focuses the window and navigates to the chat.
  • Web (desktop or mobile browser): Web Notifications API from the client (works for background tabs, e.g. phone browser over Tailscale). Clicking focuses the tab and navigates to the chat.

How

  • Detection rides the existing projectThreadAwareness() phase ladder (@t3tools/shared/agentAwareness) — the same source of truth the mobile push pipeline uses. A pure edge-detecting tracker (apps/web/src/notifications/needsAttention.logic.ts) diffs phases across thread-upserted shell updates: fires on transitions into waiting_for_approval / waiting_for_input, and on running → completed only (which also kills the spurious "Done" at thread birth). First observation of a thread never fires, so reloading with an already-waiting thread stays silent.
  • One coordinator for both surfaces: Electron renders the same web bundle, so a single <NotificationCoordinator/> in the root route watches all threads across all connected environments and dispatches through a platform shim — desktopBridge.showNotification(...) when present, else new Notification(...) (guarded: Android Chrome's page-scope constructor throw is swallowed).
  • Desktop IPC: new desktop:show-notification / desktop:notification-activated channels, an ElectronNotification Effect service (coalesces per-thread via the request id, retains live references), preload bridge members typed in @t3tools/contracts (optional, so older shells hosting newer bundles degrade to the web path).
  • Suppression: no notification when the tab/window is focused and that exact chat is on screen (including the draft route right after promotion). Different chat on screen → still notifies.
  • Settings:notifyOnNeedsInput client setting (default on), row in Settings → General ("Notify when an agent needs input"). On web, enabling requests browser permission from the toggle gesture; a denied permission shows an inline hint; a once-per-session toast nudges permission when a notification would have fired but permission was never asked.

Out of scope (per issue): Web Push for closed tabs (needs service worker + push infra; iOS Safari additionally requires an installed PWA).

Testing

  • Unit: 40+ new tests across the tracker (every behavior rule + mutation-tested edges), the notifier shim (platform routing, constructor-throw guard), coordinator pure helpers (input building/dedupe, activation params), permission-prompt truth table, contracts decode defaults, and the Electron service/IPC method (fake-Electron harness: click→reveal→broadcast, coalescing, unsupported guard). Full suites green: web 172 files / 1505 tests, desktop 53 / 377, contracts 202. Typecheck + lint clean on all touched packages.
  • Live end-to-end (real browser, real Claude CLI turns, isolated --home-dir instance): turn-completion and agent-question notifications fire exactly once with correct title/body/tag while the tab is genuinely blurred; startup grace on reload; suppression when viewing the firing chat (and the converse — other-route transitions do notify); settings toggle gates and persists; notification click navigates to the right thread. Re-smoked after the final fix wave.
  • Desktop:pnpm build:desktop + pnpm test:desktop-smoke pass. (Native notification click path is unit-tested via fakes; not OS-driven e2e.)

Review process

Built task-by-task with per-task spec+quality reviews, a final whole-branch review, and a verified fix wave (notably: Android Chrome constructor guard, desktop Notification Center coalescing, draft-route suppression). Known accepted edges: notifications for since-deleted threads navigate to a dead route; a macOS click with zero open windows doesn't recreate one; permission state in the settings panel refreshes on interaction, not on browser-chrome changes.

🤖 Generated with Claude Code

radroidand others added 6 commits July 25, 2026 09:53
…ification surface
Adds the `notifyOnNeedsInput` client setting (default on, opt-out) to
`ClientSettingsSchema` and `ClientSettingsPatch`, plus the renderer-facing
`DesktopNotificationRequest` / `DesktopNotificationActivation` types and the
optional `showNotification` / `onNotificationActivated` members on
`DesktopBridge`. Both bridge members are optional like `preview` so a newer
web bundle can run inside an older desktop shell and feature-detect.
The desktop `ClientSettings` test fixture gains the new required field so the
repo still typechecks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…avigate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…top coalescing, draft-route suppression
Guard `new Notification(...)` against Android Chrome's illegal-constructor
throw, coalesce desktop notifications by request id, resolve the active
thread through the draft route, hide the denied hint while the toggle is
off, and pin the shim's platform routing with tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 414baf24-9726-445c-833e-17e65b3ea373

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/chat-input-notifications

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit 761b46c into mainJul 26, 2026
1 check passed
radroid added a commit that referenced this pull request Jul 27, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Jul 28, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroid deleted the t3code/chat-input-notifications branch July 30, 2026 14:23
github-actionsBot pushed a commit that referenced this pull request Aug 2, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 3, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto 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.

Notify the user when a chat needs input (desktop + web/mobile browser)

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: notify when a chat needs input (desktop + web) - #14

Merged
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications
Jul 26, 2026
Merged

feat: notify when a chat needs input (desktop + web)#14
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#11

What

Fires a notification when a chat transitions into a state that needs you — approval requested, agent question (waiting for input), or turn finished — on both surfaces:

  • Desktop (Electron): native OS notification from the main process. Clicking it reveals/focuses the window and navigates to the chat.
  • Web (desktop or mobile browser): Web Notifications API from the client (works for background tabs, e.g. phone browser over Tailscale). Clicking focuses the tab and navigates to the chat.

How

  • Detection rides the existing projectThreadAwareness() phase ladder (@t3tools/shared/agentAwareness) — the same source of truth the mobile push pipeline uses. A pure edge-detecting tracker (apps/web/src/notifications/needsAttention.logic.ts) diffs phases across thread-upserted shell updates: fires on transitions into waiting_for_approval / waiting_for_input, and on running → completed only (which also kills the spurious "Done" at thread birth). First observation of a thread never fires, so reloading with an already-waiting thread stays silent.
  • One coordinator for both surfaces: Electron renders the same web bundle, so a single <NotificationCoordinator/> in the root route watches all threads across all connected environments and dispatches through a platform shim — desktopBridge.showNotification(...) when present, else new Notification(...) (guarded: Android Chrome's page-scope constructor throw is swallowed).
  • Desktop IPC: new desktop:show-notification / desktop:notification-activated channels, an ElectronNotification Effect service (coalesces per-thread via the request id, retains live references), preload bridge members typed in @t3tools/contracts (optional, so older shells hosting newer bundles degrade to the web path).
  • Suppression: no notification when the tab/window is focused and that exact chat is on screen (including the draft route right after promotion). Different chat on screen → still notifies.
  • Settings:notifyOnNeedsInput client setting (default on), row in Settings → General ("Notify when an agent needs input"). On web, enabling requests browser permission from the toggle gesture; a denied permission shows an inline hint; a once-per-session toast nudges permission when a notification would have fired but permission was never asked.

Out of scope (per issue): Web Push for closed tabs (needs service worker + push infra; iOS Safari additionally requires an installed PWA).

Testing

  • Unit: 40+ new tests across the tracker (every behavior rule + mutation-tested edges), the notifier shim (platform routing, constructor-throw guard), coordinator pure helpers (input building/dedupe, activation params), permission-prompt truth table, contracts decode defaults, and the Electron service/IPC method (fake-Electron harness: click→reveal→broadcast, coalescing, unsupported guard). Full suites green: web 172 files / 1505 tests, desktop 53 / 377, contracts 202. Typecheck + lint clean on all touched packages.
  • Live end-to-end (real browser, real Claude CLI turns, isolated --home-dir instance): turn-completion and agent-question notifications fire exactly once with correct title/body/tag while the tab is genuinely blurred; startup grace on reload; suppression when viewing the firing chat (and the converse — other-route transitions do notify); settings toggle gates and persists; notification click navigates to the right thread. Re-smoked after the final fix wave.
  • Desktop:pnpm build:desktop + pnpm test:desktop-smoke pass. (Native notification click path is unit-tested via fakes; not OS-driven e2e.)

Review process

Built task-by-task with per-task spec+quality reviews, a final whole-branch review, and a verified fix wave (notably: Android Chrome constructor guard, desktop Notification Center coalescing, draft-route suppression). Known accepted edges: notifications for since-deleted threads navigate to a dead route; a macOS click with zero open windows doesn't recreate one; permission state in the settings panel refreshes on interaction, not on browser-chrome changes.

🤖 Generated with Claude Code

radroidand others added 6 commits July 25, 2026 09:53
…ification surface
Adds the `notifyOnNeedsInput` client setting (default on, opt-out) to
`ClientSettingsSchema` and `ClientSettingsPatch`, plus the renderer-facing
`DesktopNotificationRequest` / `DesktopNotificationActivation` types and the
optional `showNotification` / `onNotificationActivated` members on
`DesktopBridge`. Both bridge members are optional like `preview` so a newer
web bundle can run inside an older desktop shell and feature-detect.
The desktop `ClientSettings` test fixture gains the new required field so the
repo still typechecks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…avigate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…top coalescing, draft-route suppression
Guard `new Notification(...)` against Android Chrome's illegal-constructor
throw, coalesce desktop notifications by request id, resolve the active
thread through the draft route, hide the denied hint while the toggle is
off, and pin the shim's platform routing with tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 414baf24-9726-445c-833e-17e65b3ea373

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/chat-input-notifications

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit 761b46c into mainJul 26, 2026
1 check passed
radroid added a commit that referenced this pull request Jul 27, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Jul 28, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroid deleted the t3code/chat-input-notifications branch July 30, 2026 14:23
github-actionsBot pushed a commit that referenced this pull request Aug 2, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 3, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto 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.

Notify the user when a chat needs input (desktop + web/mobile browser)

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: notify when a chat needs input (desktop + web) - #14

Merged
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications
Jul 26, 2026
Merged

feat: notify when a chat needs input (desktop + web)#14
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#11

What

Fires a notification when a chat transitions into a state that needs you — approval requested, agent question (waiting for input), or turn finished — on both surfaces:

  • Desktop (Electron): native OS notification from the main process. Clicking it reveals/focuses the window and navigates to the chat.
  • Web (desktop or mobile browser): Web Notifications API from the client (works for background tabs, e.g. phone browser over Tailscale). Clicking focuses the tab and navigates to the chat.

How

  • Detection rides the existing projectThreadAwareness() phase ladder (@t3tools/shared/agentAwareness) — the same source of truth the mobile push pipeline uses. A pure edge-detecting tracker (apps/web/src/notifications/needsAttention.logic.ts) diffs phases across thread-upserted shell updates: fires on transitions into waiting_for_approval / waiting_for_input, and on running → completed only (which also kills the spurious "Done" at thread birth). First observation of a thread never fires, so reloading with an already-waiting thread stays silent.
  • One coordinator for both surfaces: Electron renders the same web bundle, so a single <NotificationCoordinator/> in the root route watches all threads across all connected environments and dispatches through a platform shim — desktopBridge.showNotification(...) when present, else new Notification(...) (guarded: Android Chrome's page-scope constructor throw is swallowed).
  • Desktop IPC: new desktop:show-notification / desktop:notification-activated channels, an ElectronNotification Effect service (coalesces per-thread via the request id, retains live references), preload bridge members typed in @t3tools/contracts (optional, so older shells hosting newer bundles degrade to the web path).
  • Suppression: no notification when the tab/window is focused and that exact chat is on screen (including the draft route right after promotion). Different chat on screen → still notifies.
  • Settings:notifyOnNeedsInput client setting (default on), row in Settings → General ("Notify when an agent needs input"). On web, enabling requests browser permission from the toggle gesture; a denied permission shows an inline hint; a once-per-session toast nudges permission when a notification would have fired but permission was never asked.

Out of scope (per issue): Web Push for closed tabs (needs service worker + push infra; iOS Safari additionally requires an installed PWA).

Testing

  • Unit: 40+ new tests across the tracker (every behavior rule + mutation-tested edges), the notifier shim (platform routing, constructor-throw guard), coordinator pure helpers (input building/dedupe, activation params), permission-prompt truth table, contracts decode defaults, and the Electron service/IPC method (fake-Electron harness: click→reveal→broadcast, coalescing, unsupported guard). Full suites green: web 172 files / 1505 tests, desktop 53 / 377, contracts 202. Typecheck + lint clean on all touched packages.
  • Live end-to-end (real browser, real Claude CLI turns, isolated --home-dir instance): turn-completion and agent-question notifications fire exactly once with correct title/body/tag while the tab is genuinely blurred; startup grace on reload; suppression when viewing the firing chat (and the converse — other-route transitions do notify); settings toggle gates and persists; notification click navigates to the right thread. Re-smoked after the final fix wave.
  • Desktop:pnpm build:desktop + pnpm test:desktop-smoke pass. (Native notification click path is unit-tested via fakes; not OS-driven e2e.)

Review process

Built task-by-task with per-task spec+quality reviews, a final whole-branch review, and a verified fix wave (notably: Android Chrome constructor guard, desktop Notification Center coalescing, draft-route suppression). Known accepted edges: notifications for since-deleted threads navigate to a dead route; a macOS click with zero open windows doesn't recreate one; permission state in the settings panel refreshes on interaction, not on browser-chrome changes.

🤖 Generated with Claude Code

radroidand others added 6 commits July 25, 2026 09:53
…ification surface
Adds the `notifyOnNeedsInput` client setting (default on, opt-out) to
`ClientSettingsSchema` and `ClientSettingsPatch`, plus the renderer-facing
`DesktopNotificationRequest` / `DesktopNotificationActivation` types and the
optional `showNotification` / `onNotificationActivated` members on
`DesktopBridge`. Both bridge members are optional like `preview` so a newer
web bundle can run inside an older desktop shell and feature-detect.
The desktop `ClientSettings` test fixture gains the new required field so the
repo still typechecks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…avigate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…top coalescing, draft-route suppression
Guard `new Notification(...)` against Android Chrome's illegal-constructor
throw, coalesce desktop notifications by request id, resolve the active
thread through the draft route, hide the denied hint while the toggle is
off, and pin the shim's platform routing with tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 414baf24-9726-445c-833e-17e65b3ea373

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/chat-input-notifications

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit 761b46c into mainJul 26, 2026
1 check passed
radroid added a commit that referenced this pull request Jul 27, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Jul 28, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroid deleted the t3code/chat-input-notifications branch July 30, 2026 14:23
github-actionsBot pushed a commit that referenced this pull request Aug 2, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 3, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto 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.

Notify the user when a chat needs input (desktop + web/mobile browser)

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: notify when a chat needs input (desktop + web) - #14

Merged
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications
Jul 26, 2026
Merged

feat: notify when a chat needs input (desktop + web)#14
radroid merged 6 commits into
mainfrom
t3code/chat-input-notifications

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#11

What

Fires a notification when a chat transitions into a state that needs you — approval requested, agent question (waiting for input), or turn finished — on both surfaces:

  • Desktop (Electron): native OS notification from the main process. Clicking it reveals/focuses the window and navigates to the chat.
  • Web (desktop or mobile browser): Web Notifications API from the client (works for background tabs, e.g. phone browser over Tailscale). Clicking focuses the tab and navigates to the chat.

How

  • Detection rides the existing projectThreadAwareness() phase ladder (@t3tools/shared/agentAwareness) — the same source of truth the mobile push pipeline uses. A pure edge-detecting tracker (apps/web/src/notifications/needsAttention.logic.ts) diffs phases across thread-upserted shell updates: fires on transitions into waiting_for_approval / waiting_for_input, and on running → completed only (which also kills the spurious "Done" at thread birth). First observation of a thread never fires, so reloading with an already-waiting thread stays silent.
  • One coordinator for both surfaces: Electron renders the same web bundle, so a single <NotificationCoordinator/> in the root route watches all threads across all connected environments and dispatches through a platform shim — desktopBridge.showNotification(...) when present, else new Notification(...) (guarded: Android Chrome's page-scope constructor throw is swallowed).
  • Desktop IPC: new desktop:show-notification / desktop:notification-activated channels, an ElectronNotification Effect service (coalesces per-thread via the request id, retains live references), preload bridge members typed in @t3tools/contracts (optional, so older shells hosting newer bundles degrade to the web path).
  • Suppression: no notification when the tab/window is focused and that exact chat is on screen (including the draft route right after promotion). Different chat on screen → still notifies.
  • Settings:notifyOnNeedsInput client setting (default on), row in Settings → General ("Notify when an agent needs input"). On web, enabling requests browser permission from the toggle gesture; a denied permission shows an inline hint; a once-per-session toast nudges permission when a notification would have fired but permission was never asked.

Out of scope (per issue): Web Push for closed tabs (needs service worker + push infra; iOS Safari additionally requires an installed PWA).

Testing

  • Unit: 40+ new tests across the tracker (every behavior rule + mutation-tested edges), the notifier shim (platform routing, constructor-throw guard), coordinator pure helpers (input building/dedupe, activation params), permission-prompt truth table, contracts decode defaults, and the Electron service/IPC method (fake-Electron harness: click→reveal→broadcast, coalescing, unsupported guard). Full suites green: web 172 files / 1505 tests, desktop 53 / 377, contracts 202. Typecheck + lint clean on all touched packages.
  • Live end-to-end (real browser, real Claude CLI turns, isolated --home-dir instance): turn-completion and agent-question notifications fire exactly once with correct title/body/tag while the tab is genuinely blurred; startup grace on reload; suppression when viewing the firing chat (and the converse — other-route transitions do notify); settings toggle gates and persists; notification click navigates to the right thread. Re-smoked after the final fix wave.
  • Desktop:pnpm build:desktop + pnpm test:desktop-smoke pass. (Native notification click path is unit-tested via fakes; not OS-driven e2e.)

Review process

Built task-by-task with per-task spec+quality reviews, a final whole-branch review, and a verified fix wave (notably: Android Chrome constructor guard, desktop Notification Center coalescing, draft-route suppression). Known accepted edges: notifications for since-deleted threads navigate to a dead route; a macOS click with zero open windows doesn't recreate one; permission state in the settings panel refreshes on interaction, not on browser-chrome changes.

🤖 Generated with Claude Code

radroidand others added 6 commits July 25, 2026 09:53
…ification surface
Adds the `notifyOnNeedsInput` client setting (default on, opt-out) to
`ClientSettingsSchema` and `ClientSettingsPatch`, plus the renderer-facing
`DesktopNotificationRequest` / `DesktopNotificationActivation` types and the
optional `showNotification` / `onNotificationActivated` members on
`DesktopBridge`. Both bridge members are optional like `preview` so a newer
web bundle can run inside an older desktop shell and feature-detect.
The desktop `ClientSettings` test fixture gains the new required field so the
repo still typechecks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…avigate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…top coalescing, draft-route suppression
Guard `new Notification(...)` against Android Chrome's illegal-constructor
throw, coalesce desktop notifications by request id, resolve the active
thread through the draft route, hide the denied hint while the toggle is
off, and pin the shim's platform routing with tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 414baf24-9726-445c-833e-17e65b3ea373

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/chat-input-notifications

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit 761b46c into mainJul 26, 2026
1 check passed
radroid added a commit that referenced this pull request Jul 27, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Jul 28, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroid deleted the t3code/chat-input-notifications branch July 30, 2026 14:23
github-actionsBot pushed a commit that referenced this pull request Aug 2, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 3, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actionsBot pushed a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 10, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
… push)
Closes the accepted v1 gap from #14: notifications previously only fired while
a T3 tab was open. This delivers a push when the tab is fully closed.
Client (apps/web):
- PWA manifest + icons + installability (required for iOS Safari Web Push).
- Push-only service worker (no fetch handler, so no offline/stale-asset risk):
shows a notification on push, focuses/opens the waiting thread on click, and
suppresses when any T3 tab is open so the in-page NotificationCoordinator
stays the single source while a tab is alive.
- PushSubscriptionManager keeps the subscription in sync with the existing
notifyOnNeedsInput setting + permission (reacts to a post-mount grant via the
Permissions API). Pure, tested VAPID/subscription helpers.
Server (apps/server/src/t3x/webPush — fork-seam clean, no upstream edits):
- Reactor taps OrchestrationEngine.streamDomainEvents, recomputes the awareness
phase with the shared projectThreadAwareness, edge-detects the same
transitions the web client does (waiting_for_input/approval; running->
completed), and sends Web Push to registered subscriptions, pruning expired
(404/410) ones.
- JSON subscription store in the state dir; VAPID keypair via ServerSecretStore
(T3X_VAPID_* env override supported); raw
/api/t3x/push/{subscribe,unsubscribe,vapid-public-key} routes mounted through
the existing T3xRoutesLive seam.
Verified: apps/web + apps/server typecheck clean (0 errors); web + server unit
tests pass; web-push loads at runtime; server+bin construction tests (277) pass.
Browser end-to-end (grant permission, close tab, receive push) is manual QA.
Closes#19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto 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.

Notify the user when a chat needs input (desktop + web/mobile browser)

1 participant

@radroid