Skip to content

Add browser notifications for thread events#148

Open
friuns2 wants to merge 2 commits intomainfrom
codex/browser-thread-notifications
Open

Add browser notifications for thread events#148
friuns2 wants to merge 2 commits intomainfrom
codex/browser-thread-notifications

Conversation

@friuns2
Copy link
Copy Markdown
Owner

@friuns2 friuns2 commented May 9, 2026

Summary

  • add opt-in browser notifications for completed background turns
  • notify when Codex needs approval or a user response
  • add settings UI and manual test coverage

Verification

  • pnpm run build:frontend
  • git diff --check

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 9, 2026

Review Summary by Qodo

(Agentic_describe updated until commit f0ee123)

Add browser notifications for thread events

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add opt-in browser notifications for completed background turns
• Notify when Codex needs approval or user response
• Add settings UI toggle with permission management
• Suppress notifications for currently visible selected thread
Diagram
flowchart LR
  A["Thread Events<br/>Turn Completed<br/>Pending Request"] -->|"notifyTurnCompleted<br/>notifyPendingServerRequest"| B["showBrowserNotification"]
  B -->|"Check Permission<br/>& Enabled State"| C["Browser Notification"]
  D["Settings UI<br/>Toggle Button"] -->|"setBrowserNotificationsEnabled"| E["Permission Request<br/>& Storage"]
  F["Notification Click"] -->|"THREAD_NOTIFICATION_CLICK_EVENT"| G["Select Thread<br/>& Navigate"]
Loading

Grey Divider

File Changes

1. src/composables/useDesktopState.ts ✨ Enhancement +122/-0

Core notification logic and state management

• Add browser notification permission and enabled state management with localStorage persistence
• Implement notification display functions with permission checks and thread-specific filtering
• Add notification handlers for completed turns and pending server requests
• Implement setBrowserNotificationsEnabled() and refreshBrowserNotificationPermission()
 functions
• Export new reactive refs and methods for notification state and control

src/composables/useDesktopState.ts


2. src/App.vue ✨ Enhancement +68/-0

Settings UI and notification event handling

• Add browser notifications settings UI button with toggle control and status display
• Implement notification click event listener to navigate to relevant thread
• Add computed properties for notification status and help text display
• Refresh notification permission on visibility change and window focus
• Integrate notification permission state into settings panel with disabled state handling

src/App.vue


3. tests.md 🧪 Tests +36/-0

Manual test coverage for notifications

• Add comprehensive manual test coverage for browser notifications feature
• Document setup prerequisites, test steps, and expected results
• Include theme testing for light and dark mode UI readability
• Cover notification suppression, click handling, and permission management

tests.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 9, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0)

Grey Divider


Action required

1. Success notify on failure 🐞 Bug ≡ Correctness ⭐ New
Description
useDesktopState.applyRealtimeUpdates() calls notifyTurnCompleted() even when the same turn/completed
event indicates a failure (turn.status === 'failed'), so users can receive a misleading “Codex task
complete” notification for failed turns. This can mask errors and erode trust in the notification
signal.
Code

src/composables/useDesktopState.ts[R3818-3822]

      if (!shouldRetryWithFallback) {
        clearPendingTurnRequest(completedTurn.threadId)
        scheduleQueueStateRefresh(completedTurn.threadId)
+        notifyTurnCompleted(completedTurn)
      }
Evidence
turn/completed notifications can represent failures (readTurnErrorMessage checks for turn.status ===
'failed'), but notifyTurnCompleted() is executed whenever fallback retry is not needed, without
checking whether turnErrorMessage is present.

src/composables/useDesktopState.ts[2905-2912]
src/composables/useDesktopState.ts[3783-3836]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
A "Codex task complete" browser notification can be shown for failed turns because `notifyTurnCompleted()` is called without checking whether the turn completed with an error.

### Issue Context
`readTurnErrorMessage()` explicitly extracts an error when a `turn/completed` payload has `turn.status === 'failed'`, but the completion notification is currently sent whenever `!shouldRetryWithFallback`.

### Fix Focus Areas
- src/composables/useDesktopState.ts[3783-3836]
- src/composables/useDesktopState.ts[2905-2912]

### What to change
- Only call `notifyTurnCompleted(completedTurn)` when there is no `turnErrorMessage` (and/or when the completed turn status is successful).
- Optionally add a separate failure notification path (e.g., `notifyTurnFailed`) with an accurate title/body, or suppress notifications for failures entirely.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Notification click doesn't open thread ✓ Resolved 🐞 Bug ≡ Correctness
Description
Notification click uses setSelectedThreadId(threadId), but App.vue route-sync logic enforces
selectedThreadId === '' on the 'home'/'skills' routes, so the selection can be immediately cleared
instead of navigating to the thread.
Code

src/composables/useDesktopState.ts[R2136-2141]

+    notification.onclick = () => {
+      window.focus()
+      if (threadId && threadId !== GLOBAL_SERVER_REQUEST_SCOPE) {
+        setSelectedThreadId(threadId)
+      }
+      notification.close()
Evidence
The notification click handler only calls setSelectedThreadId(). In App.vue,
syncThreadSelectionWithRoute() clears any non-empty selectedThreadId when the current route is
'home' or 'skills', and the selectedThreadId→router sync watcher is explicitly disabled on those
routes—so a click while on home/skills will not navigate to /thread/:threadId and can be reverted
back to ''.

src/composables/useDesktopState.ts[2126-2142]
src/App.vue[3929-3964]
src/App.vue[3993-4010]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Clicking a browser notification only calls `setSelectedThreadId(threadId)`. When the app is currently on the `home`/`skills` route, App.vue route-sync logic forces `selectedThreadId` back to `''`, so the click does not reliably open the intended thread.
## Issue Context
- The router uses hash history with a `thread` route (`/thread/:threadId`).
- `syncThreadSelectionWithRoute()` clears selection on `home`/`skills`.
## Fix Focus Areas
- src/composables/useDesktopState.ts[2136-2141]
- src/App.vue[3929-3964]
- src/App.vue[3993-4010]
## Suggested fix
On notification click, change navigation (e.g., set `window.location.hash` to `#/thread/${encodeURIComponent(threadId)}` or dispatch a global event that App.vue handles by routing to `{ name: 'thread', params: { threadId } }` and calling `selectThread(threadId)`). Ensure it works even when the current route is `home`/`skills`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Notification exposes thread preview 🐞 Bug ⛨ Security ⭐ New
Description
Browser notifications use thread title/preview as the Notification body, which can surface sensitive
user content on the OS notification UI (lock screen, shared screens, screen recordings). This is a
privacy/security exposure even though the feature is opt-in.
Code

src/composables/useDesktopState.ts[R2117-2120]

+  function threadNotificationTitle(threadId: string): string {
+    const thread = findThreadById(threadId)
+    return threadTitleById.value[threadId] || thread?.title || thread?.preview || 'Codex thread'
+  }
Evidence
threadNotificationTitle() falls back to thread.preview, and both notifyPendingServerRequest() and
notifyTurnCompleted() pass threadNotificationTitle(...) as the Notification body, so message-derived
previews can be displayed outside the app context.

src/composables/useDesktopState.ts[2111-2120]
src/composables/useDesktopState.ts[2150-2168]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Notification bodies may include `thread.preview` (often derived from user content), which can expose sensitive information via OS-level notifications.

### Issue Context
`threadNotificationTitle()` currently falls back to `thread?.preview`, and that value is used as the notification `body` for completion and pending-action notifications.

### Fix Focus Areas
- src/composables/useDesktopState.ts[2111-2120]
- src/composables/useDesktopState.ts[2150-2168]

### What to change
- Remove `thread?.preview` from the fallback chain for notification body text, or replace the body with a generic string (e.g., "Open Codex to view details") or a sanitized/thread-title-only variant.
- If showing content is desired, consider adding a separate user setting such as “Show message previews in notifications” (default off).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Notification creation can crash updates ✓ Resolved 🐞 Bug ☼ Reliability
Description
showBrowserNotification() constructs new window.Notification(...) without guarding exceptions; if
construction throws, it can break applyRealtimeUpdates() processing for server notifications and
turn completion events.
Code

src/composables/useDesktopState.ts[R2132-2135]

+    const notification = new window.Notification(title, {
+      icon: '/icons/pwa-192x192.png',
+      ...notificationOptions,
+    })
Evidence
showBrowserNotification() directly constructs a Notification. It is invoked from realtime
notification handlers (server/request and turn completion) inside applyRealtimeUpdates(); there is
no try/catch around the call sites, so any thrown exception can interrupt the realtime update flow.

src/composables/useDesktopState.ts[2126-2143]
src/composables/useDesktopState.ts[3138-3145]
src/composables/useDesktopState.ts[3716-3720]
src/composables/useDesktopState.ts[3813-3817]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`showBrowserNotification()` calls `new window.Notification(...)` without error handling. If the browser throws during construction, the exception can propagate through realtime handlers and disrupt state updates.
## Issue Context
`showBrowserNotification()` is called from `handleServerRequestNotification()` and turn completion handling in `applyRealtimeUpdates()`.
## Fix Focus Areas
- src/composables/useDesktopState.ts[2126-2143]
- src/composables/useDesktopState.ts[3138-3145]
- src/composables/useDesktopState.ts[3813-3817]
## Suggested fix
Wrap Notification creation (and any immediate property assignment like `onclick`) in `try/catch`, and on failure just return (optionally log at debug level). Keep the permission/support checks, but don’t assume they prevent runtime exceptions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit f0ee123

Results up to commit N/A


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)


Action required
1. Notification click doesn't open thread ✓ Resolved 🐞 Bug ≡ Correctness
Description
Notification click uses setSelectedThreadId(threadId), but App.vue route-sync logic enforces
selectedThreadId === '' on the 'home'/'skills' routes, so the selection can be immediately cleared
instead of navigating to the thread.
Code

src/composables/useDesktopState.ts[R2136-2141]

+    notification.onclick = () => {
+      window.focus()
+      if (threadId && threadId !== GLOBAL_SERVER_REQUEST_SCOPE) {
+        setSelectedThreadId(threadId)
+      }
+      notification.close()
Evidence
The notification click handler only calls setSelectedThreadId(). In App.vue,
syncThreadSelectionWithRoute() clears any non-empty selectedThreadId when the current route is
'home' or 'skills', and the selectedThreadId→router sync watcher is explicitly disabled on those
routes—so a click while on home/skills will not navigate to /thread/:threadId and can be reverted
back to ''.

src/composables/useDesktopState.ts[2126-2142]
src/App.vue[3929-3964]
src/App.vue[3993-4010]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Clicking a browser notification only calls `setSelectedThreadId(threadId)`. When the app is currently on the `home`/`skills` route, App.vue route-sync logic forces `selectedThreadId` back to `''`, so the click does not reliably open the intended thread.
## Issue Context
- The router uses hash history with a `thread` route (`/thread/:threadId`).
- `syncThreadSelectionWithRoute()` clears selection on `home`/`skills`.
## Fix Focus Areas
- src/composables/useDesktopState.ts[2136-2141]
- src/App.vue[3929-3964]
- src/App.vue[3993-4010]
## Suggested fix
On notification click, change navigation (e.g., set `window.location.hash` to `#/thread/${encodeURIComponent(threadId)}` or dispatch a global event that App.vue handles by routing to `{ name: 'thread', params: { threadId } }` and calling `selectThread(threadId)`). Ensure it works even when the current route is `home`/`skills`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. Notification creation can crash updates ✓ Resolved 🐞 Bug ☼ Reliability
Description
showBrowserNotification() constructs new window.Notification(...) without guarding exceptions; if
construction throws, it can break applyRealtimeUpdates() processing for server notifications and
turn completion events.
Code

src/composables/useDesktopState.ts[R2132-2135]

+    const notification = new window.Notification(title, {
+      icon: '/icons/pwa-192x192.png',
+      ...notificationOptions,
+    })
Evidence
showBrowserNotification() directly constructs a Notification. It is invoked from realtime
notification handlers (server/request and turn completion) inside applyRealtimeUpdates(); there is
no try/catch around the call sites, so any thrown exception can interrupt the realtime update flow.

src/composables/useDesktopState.ts[2126-2143]
src/composables/useDesktopState.ts[3138-3145]
src/composables/useDesktopState.ts[3716-3720]
src/composables/useDesktopState.ts[3813-3817]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`showBrowserNotification()` calls `new window.Notification(...)` without error handling. If the browser throws during construction, the exception can propagate through realtime handlers and disrupt state updates.
## Issue Context
`showBrowserNotification()` is called from `handleServerRequestNotification()` and turn completion handling in `applyRealtimeUpdates()`.
## Fix Focus Areas
- src/composables/useDesktopState.ts[2126-2143]
- src/composables/useDesktopState.ts[3138-3145]
- src/composables/useDesktopState.ts[3813-3817]
## Suggested fix
Wrap Notification creation (and any immediate property assignment like `onclick`) in `try/catch`, and on failure just return (optionally log at debug level). Keep the permission/support checks, but don’t assume they prevent runtime exceptions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

Comment thread src/composables/useDesktopState.ts Outdated
@friuns2 friuns2 marked this pull request as draft May 9, 2026 21:37
@friuns2 friuns2 marked this pull request as ready for review May 9, 2026 21:37
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 9, 2026

Persistent review updated to latest commit f0ee123

Comment on lines 3818 to 3822
if (!shouldRetryWithFallback) {
clearPendingTurnRequest(completedTurn.threadId)
scheduleQueueStateRefresh(completedTurn.threadId)
notifyTurnCompleted(completedTurn)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Success notify on failure 🐞 Bug ≡ Correctness

useDesktopState.applyRealtimeUpdates() calls notifyTurnCompleted() even when the same turn/completed
event indicates a failure (turn.status === 'failed'), so users can receive a misleading “Codex task
complete” notification for failed turns. This can mask errors and erode trust in the notification
signal.
Agent Prompt
### Issue description
A "Codex task complete" browser notification can be shown for failed turns because `notifyTurnCompleted()` is called without checking whether the turn completed with an error.

### Issue Context
`readTurnErrorMessage()` explicitly extracts an error when a `turn/completed` payload has `turn.status === 'failed'`, but the completion notification is currently sent whenever `!shouldRetryWithFallback`.

### Fix Focus Areas
- src/composables/useDesktopState.ts[3783-3836]
- src/composables/useDesktopState.ts[2905-2912]

### What to change
- Only call `notifyTurnCompleted(completedTurn)` when there is no `turnErrorMessage` (and/or when the completed turn status is successful).
- Optionally add a separate failure notification path (e.g., `notifyTurnFailed`) with an accurate title/body, or suppress notifications for failures entirely.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants