Skip to content

Repository files navigation

AgentPulse

A cross-platform desktop app that brings Dynamic Island-inspired real-time monitoring to your AI coding assistant sessions.

Inspired by ClaudePulse by @tzangms — a beautiful macOS-native app built with Swift/SwiftUI. AgentPulse is a cross-platform rewrite using Tauri v2 to support Linux, Windows, and macOS, extended with multi-provider support.

Demo

AgentPulse demo

Full-quality recording with audio: assets/demo.mp4

Capsule (collapsed)
Collapsed capsule
Expanded session list
Expanded session list

Supported AI Coding Assistants

ProviderHook EventsConfig Location
Claude Code8 events~/.claude/settings.json
Antigravity CLI (agy)3 events~/.gemini/config/hooks.json
Codex CLI (OpenAI)5 events~/.codex/hooks.json + config.toml
GitHub Copilot CLI6 events~/.copilot/config.json

Provider icons from @lobehub/icons.

Hook Events per Provider

Event (normalized)ClaudeAntigravityCodexCopilot
SessionStartSessionStartSessionStartsessionStart
SessionEndSessionEndsessionEnd
UserPromptSubmitUserPromptSubmitPreInvocationUserPromptSubmituserPromptSubmitted
PreToolUsePreToolUsePreToolUsepreToolUse
PostToolUsePostToolUsePostToolUsePostToolUsepostToolUse
StopStopStopStopagentStop / subagentStop
PermissionRequestPermissionRequest
PostToolUseFailurePostToolUseFailure
NotificationerrorOccurred

All events normalized to PascalCase internally. Each provider's hook command invokes the bundled sidecar binary agent-pulse-hook, which reads the event JSON from stdin and POSTs it to http://localhost:{port}/hook/{provider}. Going through a native binary instead of an inline shell one-liner keeps hooks shell-agnostic across bash / PowerShell / cmd.exe.

Antigravity is a special case.agy uses a different hook model from every other provider: a separate hooks.json keyed by hook name → event → handlers, only 5 event types, and synchronous hooks that must print a JSON result on stdout (the stdin payload carries no event-name field — the event is the config key). AgentPulse wires only PreInvocation / PostToolUse / Stop (the events whose stdout contract is satisfied by an empty {}), passes the event name to the sidecar as a 2nd arg (agent-pulse-hook antigravity Stop), and the sidecar injects it as hook_event_name before POSTing, then echoes {}. PreToolUse is skipped — it demands a decision field a passive monitor shouldn't fabricate.

Field Name Normalization

Different CLIs use different JSON field names. AgentPulse auto-detects and normalizes:

Internal FieldAccepted Aliases
session_idsession_id, sessionId, session
hook_event_namehook_event_name, hookEventName, event, type
cwdcwd, workingDirectory, projectDir
promptprompt, initialPrompt, input, message, userPrompt
tool_nametool_name, toolName

If session_id is missing, default ID generated as {provider}-default.

Session State Machine

SessionStart ──▶ Idle
│
UserPromptSubmit / PreToolUse / PostToolUse
│
▼
Working ──Stop──▶ Idle (+ completion sound)
│
PermissionRequest
│
▼
WaitingForUser ──PreToolUse──▶ Working
(+ waiting sound on entry)

Timeout-based transitions (checked every 10 seconds):

ConditionAction
Active session, 30 sec no events→ Idle
Any session, 10 min no events→ Stale (dim gray)
Any session, 30 min no eventsRemoved from list
SessionEnd event receivedRemoved immediately
User clicks X button on sessionRemoved immediately

Hook → State mapping:

Hook EventState Change
SessionStart→ Idle (new session created)
UserPromptSubmit→ Working
PreToolUse / PostToolUse / PostToolUseFailure→ Working
PermissionRequest→ WaitingForUser (triggers waiting sound on first entry)
Stop→ Idle (triggers completion sound if was Working)
SessionEndSession removed from list

Status indicator colors (theme-adaptive via CSS variables):

StateDark themeLight theme
Workingrgb(77,242,153) (light green)rgb(20,140,80) (dark green)
Waitingrgb(255,179,64) (light orange)rgb(217,119,6) (dark orange)
Idlegray (text-dim)gray (text-dim)
Staledim graydim gray

Sound System

External MP3/WAV/OGG files in ~/.config/agentpulse/sounds/. Each provider can have two independent sounds: one for completion (Working → Idle) and one for waiting for user (any state → WaitingForUser).

Setup

  1. Open Settings → Sounds tab → enable Notification Sounds
  2. Two sections appear:
    • On Complete — dropdown per provider
    • On Waiting For User — dropdown per provider
  3. Click 📁 to open the sounds folder
  4. Drop your MP3/WAV/OGG files there
  5. Files appear in the dropdowns (each rescans on click)
  6. Click ▶ next to each provider to preview

Auto-matching

On first launch, AgentPulse auto-assigns files by filename prefix:

  • {provider}.mp3 → completion sound (e.g. claude.mp3)
  • {provider}-waiting.mp3 → waiting sound (e.g. claude-waiting.mp3)

Bundled defaults

The repo's sounds/ directory ships 8 default TTS sounds (Taiwanese voice zh-TW-HsiaoChenNeural / 曉臻):

  • 4 completion clips: {provider}.mp3 — e.g. "Claude 任務完成"
  • 4 waiting clips: {provider}-waiting.mp3 — e.g. "Claude 等待回應"

On every launch, any missing default is copied into ~/.config/agentpulse/sounds/ — so upgrading also picks up new clips automatically without clobbering user customisations.

Generate your own TTS sounds (optional)

Generate Chinese voice notifications using edge-tts:

pip install edge-tts
mkdir -p ~/.config/agentpulse/sounds
forpin claude antigravity copilot codex;do# completion
edge-tts --voice "zh-TW-HsiaoChenNeural" \
--text "${p} 任務完成" \
--write-media ~/.config/agentpulse/sounds/${p}.mp3
# waiting
edge-tts --voice "zh-TW-HsiaoChenNeural" \
--text "${p} 等待回應" \
--write-media ~/.config/agentpulse/sounds/${p}-waiting.mp3
done

Audio playback uses rodio (Rust-side, no browser CSP issues).

Telegram Notifications

Get a Telegram message when a task completes or a session waits for your input — useful when you step away from the machine. Same triggers as the sound system, independent on/off per trigger.

Message format: [AgentPulse] claude 任務完成 — ~/work/backend

Setup

  1. Create a bot: message @BotFather on Telegram → /newbot → copy the bot token (looks like 123456:ABC-DEF...)
  2. Get your chat ID: send any message to your new bot, then open https://api.telegram.org/bot<TOKEN>/getUpdates in a browser — your chat ID is in result[].message.chat.id. (Or message @userinfobot.)
  3. Open Settings → Telegram tab → paste the token and chat ID
  4. Enable Notify on Complete and/or Notify on Waiting
  5. Click Send Test Message to verify — errors from the Telegram API (bad token, wrong chat ID) are shown inline

Notes:

  • Sending happens in the Rust backend (no webview CSP involvement); failures are logged and never retried — the capsule still shows the state
  • The token is stored in plain text in ~/.config/agentpulse/config.json, same as the rest of the config (single-user local app)

Features

  • Dynamic Island Style — Floating capsule expands on hover
  • Multi-Provider — Claude, Antigravity, Codex, Copilot simultaneously
  • Provider Icons — Each session shows provider's official icon (lobehub/icons)
  • Status Dot — Inline colored indicator next to project name
  • 3-Line Session Info — Project name + status, working directory, last prompt (italic)
  • Remove Session — X button appears on row hover, click to remove
  • Smart Re-render — Timer updates in-place, only structural changes trigger full re-render
  • Per-Provider Sounds — Each CLI has independent sounds for completion and waiting-for-user states (Rust rodio)
  • Telegram Notifications — Optional bot message on completion / waiting-for-user, with in-app test button
  • Single Instance — Second launch focuses the running window; no duplicate tray icons
  • Bounce Animation — Window bounces when collapsing
  • Draggable — Drag capsule anywhere
  • Light / Dark Theme — Toggle in Settings or Tray
  • System Tray — Show/Hide, Open Settings, Toggle Theme, Open Config, Restart, Quit
  • Settings (Tabbed) — Providers / Sounds / Appearance tabs
  • Open Provider Config — Button per provider opens its CLI config file
  • Open AgentPulse Config — Tray menu opens ~/.config/agentpulse/config.json
  • GitHub Link — Action bar button opens repo in browser
  • Auto-Detection — First launch detects installed CLIs via which
  • Cross-Platform — Linux, Windows, macOS (Tauri v2)

Install

v0.2 releases ship as plain zip archives — no installer, no package manager. Download, unzip, run. The zip contains the main binary plus the agent-pulse-hook sidecar; keep the two files in the same folder (the main app locates the sidecar at its own sibling path).

Linux

unzip agent-pulse-vX.Y.Z-linux.zip -d agent-pulse
cd agent-pulse
chmod +x agent-pulse agent-pulse-hook
./agent-pulse

macOS

Pick the right build for your chip:

  • Apple Silicon (M1 / M2 / M3 / M4)agent-pulse-vX.Y.Z-macos-arm64.zip
  • Intel Macagent-pulse-vX.Y.Z-macos-x64.zip
unzip agent-pulse-vX.Y.Z-macos-arm64.zip -d agent-pulse
cd agent-pulse
chmod +x agent-pulse agent-pulse-hook
# Strip the quarantine xattr Gatekeeper added on download so the app can launch
xattr -cr ./agent-pulse ./agent-pulse-hook
./agent-pulse

Why the xattr step? AgentPulse binaries aren't signed with an Apple Developer certificate ($99/yr) yet. When you download a zip from the internet, macOS tags the files with com.apple.quarantine and Gatekeeper refuses to launch them — you'll see "cannot be opened because the developer cannot be verified". Removing the xattr tells Gatekeeper to skip that check for this one file. Alternatively, right-click the binary in Finder → OpenOpen again in the confirmation dialog.

Windows

  1. Download agent-pulse-vX.Y.Z-windows.zip from the Releases page
  2. Right-click the zip → Extract All → pick any folder
  3. Double-click agent-pulse.exe (Windows SmartScreen may warn on first run since the binary isn't code-signed; click More info → Run anyway)

Latest release: Releases.

Build from Source

Prerequisites

  • Rust 1.77+
  • Node.js 18+
  • Linux dependencies:
    sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libasound2-dev
    (libasound2-dev needed by rodio for audio)

Build

git clone https://github.com/yazelin/AgentPulse.git
cd AgentPulse
npm install # installs @tauri-apps/cli — no global cargo install needed
npm run build

Output:

src-tauri/target/release/agent-pulse # binary
src-tauri/target/release/bundle/deb/ # .deb
src-tauri/target/release/bundle/rpm/ # .rpm
src-tauri/target/release/bundle/appimage/ # .AppImage

Development Workflow

Important: Frontend files (src/*) are embedded into the binary at build time. Changes to HTML/CSS/JS require either rebuild OR using watch.sh (which uses devUrl mode).

Three scripts for different workflows:

./watch.sh # DEV MODE — frontend hot-reloadable, Ctrl+R in window to refresh
./dev.sh # Build debug binary + run (any change requires this)
./dev.sh release # Build release binary + run
./reload.sh # Just restart existing binary (no rebuild) — for testing startup flow
ScriptFrontend changesRust changes
./watch.shCtrl+R refreshAuto-rebuild
./dev.shNeed rebuildNeed rebuild
./reload.shNo effect (cached in old binary)No effect

watch.sh runs npm run dev (tauri dev via the bundled @tauri-apps/cli) which serves frontend from http://localhost:1420 via npx serve. On Linux the scripts default to native Wayland (XWayland forces always-on-top but ghosts transparent windows). If the capsule sinks behind other windows on your compositor, force the XWayland path with AGENTPULSE_GDK_X11=1 ./dev.sh (or ./watch.sh).

Usage

First Launch

  1. AgentPulse opens with Settings → Providers tab. All providers start disabled; detection via which only shows a "detected" hint next to each so you know which CLIs are installed.
  2. Toggle each provider you want on — each toggle immediately writes/removes its hook config (~/.claude/settings.json, ~/.gemini/config/hooks.json, etc.). No need to toggle-off-and-on.
  3. (Optional) Switch to Sounds tab, enable Notification Sounds, customise per-provider completion and waiting clips.
  4. Close settings — the capsule is ready.

Controls

ActionEffect
Hover capsuleExpand session list
Move mouse awayCollapse (with bounce animation)
Drag capsuleReposition anywhere on screen
Click session rowSet as active session (highlight)
Hover session rowShow remove (X) button
Click X buttonRemove session from list
Pin buttonKeep panel expanded without hovering
Gear buttonOpen settings
GitHub buttonOpen repo in browser

System Tray

ItemEffect
Show/HideToggle visibility (positions at current monitor top-center)
Open SettingsShow window + open settings panel
Toggle Light/DarkCycle theme
Open Config FileOpen ~/.config/agentpulse/config.json in default editor
RestartSpawn new instance and exit current
QuitExit AgentPulse

Settings (Tabbed)

Providers tab — toggle each CLI on/off (auto installs/removes hooks), 📝 button opens that CLI's config file, auto-detection shows "detected" if the CLI binary or config dir is found, disabled providers show "coming soon" if hook setup isn't implemented yet.

Providers tab

Sounds tab — master "Notification Sounds" toggle controls both clips. Below it, two sections (On Complete and On Waiting For User) each give one dropdown per provider. Each dropdown rescans the sounds folder on click. ▶ previews, 📁 opens the folder.

Sounds tab

Telegram tab — Bot Token (masked) and Chat ID fields, independent toggles for Notify on Complete / Notify on Waiting, and a Send Test Message button that reports the Telegram API response inline. See Telegram Notifications for how to get a token and chat ID.

Appearance tab — Light theme toggle, Keep Expanded toggle (pin the panel), accent color picker (Purple / Cyan / Green / Orange / Pink), size selector (S / M / L).

Appearance tab

Architecture

How It Works

Claude Code ─┐
Antigravity │ hook cmd: `agent-pulse-hook <provider>`
Codex CLI │ ──► sidecar binary reads stdin + POSTs ──► AgentPulse HTTP Server
Copilot CLI ┘ (localhost:19280-19289)
│
▼
Session Manager
(state machine, timers)
│
▼
Capsule UI (1s polling)
  1. AgentPulse starts a TCP server on port 19280-19289 (tries each in range)
  2. Port written to ~/.agentpulse/port
  3. On provider enable, hooks written to each CLI's config file (auto-cleans existing AgentPulse hooks before re-installing)
  4. Each CLI invokes agent-pulse-hook <provider_id>, piping the event JSON on stdin
  5. The sidecar reads the port file, POSTs the JSON body to /hook/{provider_id}, swallows any network error (so a hook misfire never breaks the host CLI)
  6. Server parses URL /hook/{id} → identifies provider
  7. Field name normalization handles different CLI JSON conventions
  8. Event names normalized to common PascalCase set
  9. Session manager updates state machine
  10. UI polls state every 1 second; smart re-render only on structural changes
  11. On Working → Idle: emits task-completed with provider ID → JS plays completion sound
  12. On any → WaitingForUser: emits task-waiting with provider ID → JS plays waiting sound

Hook Installation Details

Each CLI gets the same logical command: run the sidecar binary, pass the provider id. hook_cmd(provider_id) in hooks_configurator.rs builds the string using the absolute path of agent-pulse-hook (resolved via current_exe().parent() so it always sits next to the main binary):

"/absolute/path/to/agent-pulse-hook" <provider_id>

The sidecar reads the stdin body, resolves the live port from ~/.agentpulse/port, and POSTs to /hook/<provider_id>. No bash, no PowerShell, no $(cat) — works identically on every OS.

Why a sidecar? Before v0.2, hook commands were a bash one-liner using $(cat) and curl. Each CLI on Windows executes hook commands through a different shell (Claude's optional shell: powershell field, Copilot's separate powershell field, Antigravity via sh -c / cmd /c, Codex — Windows disabled entirely). Maintaining several dialects of the same command is fragile. A native binary invocation sidesteps every shell-quoting edge case.

Claude Code (~/.claude/settings.json):

{
"hooks": {
"SessionStart": [{
"matcher": "",
"hooks": [{ "type": "command", "command": "\"/usr/bin/agent-pulse-hook\" claude", "async": true }]
}]
}
}

Antigravity CLI (~/.gemini/config/hooks.json) — note the extra hook-name level, the event-name 2nd arg, and no async (agy hooks are synchronous):

{
"agentpulse": {
"PreInvocation": [{ "type": "command", "command": "\"/usr/bin/agent-pulse-hook\" antigravity PreInvocation" }],
"PostToolUse": [{
"matcher": "",
"hooks": [{ "type": "command", "command": "\"/usr/bin/agent-pulse-hook\" antigravity PostToolUse" }]
}],
"Stop": [{ "type": "command", "command": "\"/usr/bin/agent-pulse-hook\" antigravity Stop" }]
}
}

Codex CLI (~/.codex/hooks.json + ~/.codex/config.toml):

{
"hooks": {
"SessionStart": [{
"hooks": [{ "type": "command", "command": "\"/usr/bin/agent-pulse-hook\" codex" }]
}]
}
}

And in config.toml:

[features]
hooks = true

(Codex hooks are behind a feature flag. The flag was renamed from codex_hooks to hooks in Codex v0.129; AgentPulse migrates older configs automatically. Codex v0.129+ also requires per-hook trust approval — after enabling Codex in AgentPulse, run codex once and approve the listed hooks via /hooks. OpenAI currently disables hook execution on Windows; the sidecar command is still written but won't fire until they re-enable it.)

GitHub Copilot CLI (~/.copilot/config.json — uses bash field):

{
"hooks": {
"sessionStart": [{ "type": "command", "bash": "\"/usr/bin/agent-pulse-hook\" copilot" }]
}
}

Config File

~/.config/agentpulse/config.json:

{
"setup_done": true,
"appearance": {
"accent_color": "purple",
"text_size": "medium",
"theme": "dark",
"pin_expanded": false,
"sound_enabled": true,
"provider_sounds": {
"claude": "claude.mp3",
"antigravity": "antigravity.mp3",
"codex": "__none__",
"copilot": "copilot.mp3"
},
"provider_waiting_sounds": {
"claude": "claude-waiting.mp3",
"antigravity": "antigravity-waiting.mp3",
"codex": "__none__",
"copilot": "copilot-waiting.mp3"
}
},
"providers": {
"claude": { "enabled": false, "name": "Claude Code", "settings_path": "~/.claude/settings.json" },
"antigravity": { "enabled": false, "name": "Antigravity CLI", "settings_path": "~/.gemini/config/hooks.json" },
"codex": { "enabled": false, "name": "Codex CLI", "settings_path": "~/.codex/hooks.json" },
"copilot": { "enabled": false, "name": "GitHub Copilot", "settings_path": "~/.copilot/config.json" }
},
"telegram": {
"bot_token": "",
"chat_id": "",
"notify_completed": false,
"notify_waiting": false
}
}

All providers default to enabled: false — the user explicitly turns each one on, which triggers the hook install. (provider_sounds / provider_waiting_sounds value "__none__" means the user explicitly chose no sound for that provider/state combo.)

Tech Stack

ComponentTechnology
FrameworkTauri v2
BackendRust (tokio, serde, chrono, rodio)
FrontendHTML / CSS / JS (no framework, no bundler)
HTTP Servertokio TCP (raw HTTP parsing)
WindowWebKitGTK (Linux), WebView2 (Windows), WKWebView (macOS)
Audiorodio (Rust audio playback)
Icons@lobehub/icons (inline SVG)
Linux extraswebkit2gtk, gtk, gdk crates for window management

Project Structure

AgentPulse/
├── src/ # Frontend
│ ├── index.html # Capsule, expanded, settings views (tabbed)
│ ├── styles.css # All styles, theme-adaptive via CSS vars
│ └── main.js # Tauri IPC, state, UI, provider icons, sound playback
├── src-tauri/ # Backend (Rust)
│ ├── Cargo.toml # Dependencies (tauri, rodio, webkit2gtk, etc.)
│ ├── tauri.conf.json # Window, tray, bundle, devUrl config
│ ├── capabilities/default.json # Tauri v2 permissions
│ └── src/
│ ├── lib.rs # App setup, tray, window mgmt, all Tauri commands
│ ├── config.rs # Config R/W, provider detection, sounds dir, default seeding
│ ├── hook_server.rs # TCP HTTP server, URL routing, event normalization
│ ├── hook_event.rs # RawHookEvent (field aliases) → HookEvent (normalized)
│ ├── session.rs # State machine, SessionManager, SessionTransition, AppState
│ ├── hooks_configurator.rs # Per-provider hook install/remove (4 different formats)
│ └── bin/
│ └── agent-pulse-hook.rs # Sidecar binary invoked by CLI hooks; POSTs to localhost
├── sounds/ # Bundled default TTS sounds (zh-TW HsiaoChen voice)
│ ├── claude.mp3 # completion clips
│ ├── antigravity.mp3
│ ├── codex.mp3
│ ├── copilot.mp3
│ ├── claude-waiting.mp3 # waiting-for-user clips
│ ├── antigravity-waiting.mp3
│ ├── codex-waiting.mp3
│ └── copilot-waiting.mp3
├── watch.sh # Dev mode with frontend hot-reload (devUrl)
├── dev.sh # Build debug/release binary + run
├── reload.sh # Restart existing binary (no rebuild)
├── package.json
├── README.md
└── LICENSE

Known Limitations (Linux / X11)

IssueWorkaround
rgba() backgrounds ghost on transparent windowsOpaque rgb() backgrounds; transparent window only for rounded corners
CSS -webkit-app-region: drag doesn't workTauri startDragging() API via JS mousedown
mouseleave unreliable on transparent windowsTauri cursor-left event from Rust polling thread
<select> dropdown uses system native stylingCustom div-based dropdown
CSS transition / animation causes pixel ghostingMost transitions removed; bounce via Rust set_position thread
GNOME Wayland ignores always-on-top for regular app windowsScripts default to native Wayland (no ghosting). Opt into XWayland — where _NET_WM_STATE_ABOVE is respected — with AGENTPULSE_GDK_X11=1 ./dev.sh. Trade-off: XWayland forces always-on-top but transparent windows may ghost on some GPUs
transform: translateZ(0) creates black compositing layersNot used
DOM re-render destroys hover stateSmart re-render: structural changes only; timers update in-place
Browser CSP blocks blob URLs and local filesAudio plays via Rust rodio (no browser audio at all)
Click-to-focus terminal windowNot implemented — gnome-terminal-server architecture makes per-window PID lookup unreliable

Credits

License

MIT

About

Cross-platform Dynamic Island monitor for AI coding assistant sessions (Claude, Gemini, Copilot, Codex)

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages