Skip to content

feat: consolidate notifications onto hook events with expanded settings#1145

Merged
arnestrickmann merged 7 commits intomainfrom
emdash/notifications-a2u
Feb 28, 2026
Merged

feat: consolidate notifications onto hook events with expanded settings#1145
arnestrickmann merged 7 commits intomainfrom
emdash/notifications-a2u

Conversation

@rabanspiegel
Copy link
Contributor

@rabanspiegel rabanspiegel commented Feb 27, 2026

Summary

  • Consolidates notification delivery onto the hook-based event pipeline (AgentEventService), removing the unreliable PTY exit-0 path that rarely fired
  • Expands notification settings with separate toggles for sound and OS banners, plus a sound timing mode (always vs only when unfocused)
  • OS notification banners now trigger on hook stop and notification events (permission prompts, idle prompts, elicitation dialogs) when the app is unfocused
  • Sound playback respects a new focus mode setting — can be set to play always or only when the app isn't focused
  • App-level focus detection computed in main process via BrowserWindow.getAllWindows().some(w => w.isFocused()) and passed as IPC metadata

Changed files

  • src/main/settings.ts — Added osNotifications and soundFocusMode to settings schema and normalization
  • src/main/services/AgentEventService.ts — Added OS banner notifications, app focus detection, metadata broadcasting
  • src/main/services/ptyIpc.ts — Removed old showCompletionNotification() and its call site
  • src/main/preload.ts — Updated onAgentEvent to pass { appFocused } metadata
  • src/renderer/lib/soundPlayer.ts — Added focus mode gating
  • src/renderer/hooks/useAgentEvents.ts — Wired appFocused into sound player
  • src/renderer/components/NotificationSettingsCard.tsx — Expanded from single toggle to full settings card
  • src/renderer/App.tsx — Derives sound enabled state from settings on load
  • src/renderer/types/electron-api.d.ts — Updated notification types and onAgentEvent signature
  • src/test/main/ptyIpc.test.ts — Updated test expectations for removed notification path

Test plan

  • Toggle sound off → no audio on hook events
  • Toggle sound timing to "Only when unfocused" → sounds only play when app is alt-tabbed
  • Toggle OS notifications off → no system banners
  • Toggle master off → everything silent, sub-settings dimmed
  • pnpm run type-check && pnpm run lint && pnpm exec vitest run passes

Note

Medium Risk
Adds a new localhost HTTP hook server and propagates its token/port into spawned PTYs, plus expands persisted notification settings; failures here could break notifications or introduce local event spoofing if the hook env vars leak.

Overview
Consolidates notifications around hook-driven AgentEvents instead of PTY exit. The main process now starts/stops a local AgentEventService HTTP server that authenticates hook callbacks via a per-run token, normalizes payload fields, computes focus state, and broadcasts agent:event (with { appFocused }) to all renderer windows.

Enables provider hooks and new notification controls. PTY spawns now receive EMDASH_HOOK_PORT/EMDASH_HOOK_TOKEN/EMDASH_PTY_ID env vars, and Claude worktrees get an idempotent .claude/settings.local.json hook config so Notification/Stop events POST back to Emdash. Notification settings expand to include osNotifications and soundFocusMode, the UI is updated accordingly, and the renderer adds soundPlayer/useAgentEvents to play sounds and update task activity; OS banners are now shown from AgentEventService when unfocused and enabled.

Written by Cursor Bugbot for commit f14872b. This will update automatically on new commits. Configure here.

Adds an HTTP relay server (AgentEventService) that receives structured
events from Claude Code's Notification and Stop hooks via curl. Events
are broadcast over IPC to the renderer, which plays synthesized tones
(needs_attention, task_complete) using the Web Audio API.

- Write .claude/settings.local.json with hook config before Claude PTY start
- Pass EMDASH_HOOK_PORT, EMDASH_PTY_ID, EMDASH_HOOK_TOKEN env vars to PTYs
- Normalize snake_case payload fields from Claude Code to camelCase
- Update activityStore to mark tasks idle on permission/stop events
- Sounds play for all tasks regardless of focus
- Existing regex-based status detection kept as fallback for non-hook providers
… focus mode

Consolidate notifications onto hook-driven pipeline. Add separate toggles
for sound and OS banner notifications, plus a sound timing mode (always vs
only when unfocused). Remove unreliable PTY exit-0 notification path.
@vercel
Copy link

vercel bot commented Feb 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Feb 28, 2026 0:13am

Request Review

@greptile-apps
Copy link

greptile-apps bot commented Feb 27, 2026

Greptile Summary

This PR successfully consolidates notification delivery onto a hook-based event pipeline, replacing the unreliable PTY exit-0 notification path with a more robust HTTP callback system. The implementation adds a new AgentEventService that listens for hook callbacks from Claude Code, normalizes payloads, computes app focus state, and broadcasts events to the renderer for sound playback and OS notifications.

Key improvements:

  • New settings for OS notifications and sound timing (always vs only when unfocused)
  • App-level focus detection in main process passed as IPC metadata
  • Separate control over sound and OS banner notifications
  • Clean removal of old PTY exit-based notification code
  • Proper environment variable passing for hook callbacks

Architecture:
The flow works as follows: Claude Code hooks → HTTP POST to AgentEventService → IPC broadcast with focus metadata → renderer processes events → soundPlayer plays audio (respecting focus mode) + OS notifications shown when app unfocused.

The implementation is clean, well-structured, and follows the codebase patterns. Settings normalization handles edge cases properly, and error handling is appropriate throughout.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation is well-architected with proper separation of concerns, comprehensive error handling, and security measures (token auth, payload size limits, input validation). The changes follow established codebase patterns, types are properly defined, and tests are updated accordingly. No critical bugs or security vulnerabilities identified.
  • No files require special attention

Important Files Changed

Filename Overview
src/main/services/AgentEventService.ts New HTTP service for receiving hook callbacks from agents, with token auth, payload normalization, and OS notification delivery
src/main/services/ClaudeHookService.ts Writes Claude Code hook configuration to worktree .claude/settings.local.json with curl commands for event callbacks
src/main/settings.ts Added osNotifications and soundFocusMode fields to notification settings with proper defaults and normalization
src/main/services/ptyIpc.ts Removed old PTY exit-based notification code, added Claude hook config write on PTY start
src/renderer/lib/soundPlayer.ts New Web Audio API-based sound player with focus mode gating and two sound types (needs_attention, task_complete)
src/renderer/components/NotificationSettingsCard.tsx Expanded from simple toggle to full settings card with master toggle, sound, sound timing, and OS notifications

Sequence Diagram

sequenceDiagram
    participant Claude as Claude Code Agent
    participant Hook as Claude Hook (bash/curl)
    participant AES as AgentEventService (HTTP)
    participant Main as Main Process
    participant IPC as IPC Channel
    participant Renderer as Renderer Process
    participant SP as soundPlayer
    participant OS as OS Notifications

    Claude->>Hook: Trigger event (stop/notification)
    Hook->>AES: POST /hook with token + event data
    AES->>AES: Validate token & ptyId
    AES->>AES: Normalize snake_case → camelCase
    AES->>Main: Compute appFocused via BrowserWindow
    
    alt App is unfocused & OS notifications enabled
        AES->>OS: Show notification banner (silent)
    end
    
    AES->>IPC: Broadcast agent:event + {appFocused}
    IPC->>Renderer: Deliver event to all windows
    Renderer->>SP: Map event to sound (stop→task_complete, notification→needs_attention)
    
    alt Sound enabled & focus mode satisfied
        SP->>SP: Play Web Audio API tones
    end
    
    Renderer->>Renderer: Update activityStore (task busy/idle status)
Loading

Last reviewed commit: 336e062

- Use curl -d @- to pipe stdin directly as POST body instead of
  embedding payload in a shell string, preventing corruption from
  $, backticks, or backslashes in AI-generated text
- Send ptyId and event type as HTTP headers instead of in the JSON body
- Add isDestroyed guards on BrowserWindow and webContents before IPC
  send, with per-window try/catch so one closing window doesn't abort
  delivery to the rest
- Remove EMDASH_HOOK_* from AGENT_ENV_VARS passthrough list since
  they are already set explicitly in startDirectPty/startPty
Filter out existing Emdash entries by EMDASH_HOOK_PORT marker and
append a fresh one, instead of replacing the entire Notification and
Stop hook arrays. User-defined hooks for those event types are now
preserved across task starts.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

useEnv['EMDASH_HOOK_PORT'] = String(hookPort);
useEnv['EMDASH_PTY_ID'] = id;
useEnv['EMDASH_HOOK_TOKEN'] = agentEventService.getToken();
}
Copy link

Choose a reason for hiding this comment

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

Duplicated hook environment variable setup logic

Low Severity

The hook env var block (EMDASH_HOOK_PORT, EMDASH_PTY_ID, EMDASH_HOOK_TOKEN) is identically duplicated in both startDirectPty and startPty. If the env var names, token retrieval, or guard logic change, both call sites need consistent updates — a likely source of future drift. This could be a small shared helper.

Additional Locations (1)

Fix in Cursor Fix in Web

@arnestrickmann arnestrickmann merged commit 66a76e3 into main Feb 28, 2026
5 checks passed
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