@mohak34/opencode-notifier 0.1.29 → 0.1.30-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/index.js +227 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,7 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
|
|
|
54
54
|
"showProjectName": true,
|
|
55
55
|
"showSessionTitle": false,
|
|
56
56
|
"showIcon": true,
|
|
57
|
+
"suppressWhenFocused": true,
|
|
57
58
|
"notificationSystem": "osascript",
|
|
58
59
|
"linux": {
|
|
59
60
|
"grouping": false
|
|
@@ -121,6 +122,7 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
|
|
|
121
122
|
- `showProjectName` - Show folder name in notification title (default: true)
|
|
122
123
|
- `showSessionTitle` - Include the session title in notification messages via `{sessionTitle}` placeholder (default: true)
|
|
123
124
|
- `showIcon` - Show OpenCode icon, Windows/Linux only (default: true)
|
|
125
|
+
- `suppressWhenFocused` - Skip notifications and sounds when the terminal is the active window (default: true). See [Focus detection](#focus-detection) for platform details
|
|
124
126
|
- `notificationSystem` - macOS only: `"osascript"` or `"node-notifier"` (default: "osascript")
|
|
125
127
|
- `linux.grouping` - Linux only: replace notifications in-place instead of stacking (default: false). Requires `notify-send` 0.8+
|
|
126
128
|
|
|
@@ -277,6 +279,32 @@ Run your own script when something happens. Use `{event}`, `{message}`, and `{se
|
|
|
277
279
|
|
|
278
280
|
**NOTE:** If you go with node-notifier and start missing notifications, just switch back or remove the option from the config. Users have reported issues with using node-notifier for receiving only sounds and no notification popups.
|
|
279
281
|
|
|
282
|
+
## Focus detection
|
|
283
|
+
|
|
284
|
+
When `suppressWhenFocused` is `true` (the default), notifications and sounds are skipped if the terminal running OpenCode is the active/focused window. The idea is simple: if you're already looking at it, you don't need an alert.
|
|
285
|
+
|
|
286
|
+
To disable this and always get notified:
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
{
|
|
290
|
+
"suppressWhenFocused": false
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Platform support
|
|
295
|
+
|
|
296
|
+
| Platform | Method | Requirements |
|
|
297
|
+
|----------|--------|--------------|
|
|
298
|
+
| macOS | AppleScript (`System Events`) | None |
|
|
299
|
+
| Linux X11 | `xdotool` | `xdotool` installed |
|
|
300
|
+
| Linux Wayland (Hyprland) | `hyprctl activewindow` | None |
|
|
301
|
+
| Linux Wayland (Sway) | `swaymsg -t get_tree` | None |
|
|
302
|
+
| Linux Wayland (KDE) | `kdotool` | `kdotool` installed |
|
|
303
|
+
| Linux Wayland (GNOME) | Not supported | Falls back to always notifying |
|
|
304
|
+
| Windows | `GetForegroundWindow()` via PowerShell | None |
|
|
305
|
+
|
|
306
|
+
If detection fails for any reason (missing tools, unknown compositor, permissions), it falls back to always notifying. It never silently eats your notifications.
|
|
307
|
+
|
|
280
308
|
## Linux: Notification Grouping
|
|
281
309
|
|
|
282
310
|
By default, each notification appears as a separate entry. During active sessions this can create noise when multiple events fire quickly (e.g. permission + complete + question).
|
package/dist/index.js
CHANGED
|
@@ -3745,6 +3745,7 @@ var DEFAULT_CONFIG = {
|
|
|
3745
3745
|
showProjectName: true,
|
|
3746
3746
|
showSessionTitle: false,
|
|
3747
3747
|
showIcon: true,
|
|
3748
|
+
suppressWhenFocused: true,
|
|
3748
3749
|
notificationSystem: "osascript",
|
|
3749
3750
|
linux: {
|
|
3750
3751
|
grouping: false
|
|
@@ -3848,6 +3849,7 @@ function loadConfig() {
|
|
|
3848
3849
|
showProjectName: userConfig.showProjectName ?? DEFAULT_CONFIG.showProjectName,
|
|
3849
3850
|
showSessionTitle: userConfig.showSessionTitle ?? DEFAULT_CONFIG.showSessionTitle,
|
|
3850
3851
|
showIcon: userConfig.showIcon ?? DEFAULT_CONFIG.showIcon,
|
|
3852
|
+
suppressWhenFocused: userConfig.suppressWhenFocused ?? DEFAULT_CONFIG.suppressWhenFocused,
|
|
3851
3853
|
notificationSystem: userConfig.notificationSystem === "node-notifier" ? "node-notifier" : "osascript",
|
|
3852
3854
|
linux: {
|
|
3853
3855
|
grouping: typeof userConfig.linux?.grouping === "boolean" ? userConfig.linux.grouping : DEFAULT_CONFIG.linux.grouping
|
|
@@ -4198,6 +4200,228 @@ function runCommand2(config, event, message, sessionTitle, projectName) {
|
|
|
4198
4200
|
proc.unref();
|
|
4199
4201
|
}
|
|
4200
4202
|
|
|
4203
|
+
// src/focus.ts
|
|
4204
|
+
import { execSync } from "child_process";
|
|
4205
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
4206
|
+
var TERMINAL_MAP = {
|
|
4207
|
+
ghostty: { name: "Ghostty", macProcessNames: ["Ghostty", "ghostty"] },
|
|
4208
|
+
kitty: { name: "kitty", macProcessNames: ["kitty"] },
|
|
4209
|
+
alacritty: { name: "Alacritty", macProcessNames: ["Alacritty", "alacritty"] },
|
|
4210
|
+
wezterm: { name: "WezTerm", macProcessNames: ["WezTerm", "wezterm-gui"] },
|
|
4211
|
+
apple_terminal: { name: "Terminal", macProcessNames: ["Terminal"] },
|
|
4212
|
+
iterm: { name: "iTerm2", macProcessNames: ["iTerm2", "iTerm"] },
|
|
4213
|
+
warp: { name: "Warp", macProcessNames: ["Warp"] },
|
|
4214
|
+
vscode: { name: "VS Code", macProcessNames: ["Code", "Code - Insiders", "Cursor", "cursor"] },
|
|
4215
|
+
windows_terminal: { name: "Windows Terminal", macProcessNames: [] },
|
|
4216
|
+
tmux: { name: "tmux", macProcessNames: [] }
|
|
4217
|
+
};
|
|
4218
|
+
var cachedTerminal = undefined;
|
|
4219
|
+
function detectTerminal() {
|
|
4220
|
+
if (cachedTerminal !== undefined)
|
|
4221
|
+
return cachedTerminal;
|
|
4222
|
+
const env = process.env;
|
|
4223
|
+
let result = null;
|
|
4224
|
+
if (env.GHOSTTY_RESOURCES_DIR) {
|
|
4225
|
+
result = TERMINAL_MAP.ghostty;
|
|
4226
|
+
} else if (env.KITTY_PID) {
|
|
4227
|
+
result = TERMINAL_MAP.kitty;
|
|
4228
|
+
} else if (env.ALACRITTY_SOCKET || env.ALACRITTY_LOG) {
|
|
4229
|
+
result = TERMINAL_MAP.alacritty;
|
|
4230
|
+
} else if (env.WEZTERM_EXECUTABLE) {
|
|
4231
|
+
result = TERMINAL_MAP.wezterm;
|
|
4232
|
+
} else if (env.WT_SESSION) {
|
|
4233
|
+
result = TERMINAL_MAP.windows_terminal;
|
|
4234
|
+
} else if (env.VSCODE_PID || env.TERM_PROGRAM === "vscode") {
|
|
4235
|
+
result = TERMINAL_MAP.vscode;
|
|
4236
|
+
} else if (env.TERM_PROGRAM === "Apple_Terminal") {
|
|
4237
|
+
result = TERMINAL_MAP.apple_terminal;
|
|
4238
|
+
} else if (env.TERM_PROGRAM === "iTerm.app") {
|
|
4239
|
+
result = TERMINAL_MAP.iterm;
|
|
4240
|
+
} else if (env.TERM_PROGRAM === "WarpTerminal") {
|
|
4241
|
+
result = TERMINAL_MAP.warp;
|
|
4242
|
+
} else if (env.TMUX || env.TERM_PROGRAM === "tmux") {
|
|
4243
|
+
result = TERMINAL_MAP.tmux;
|
|
4244
|
+
}
|
|
4245
|
+
cachedTerminal = result;
|
|
4246
|
+
return result;
|
|
4247
|
+
}
|
|
4248
|
+
function execWithTimeout(command, timeoutMs = 500) {
|
|
4249
|
+
try {
|
|
4250
|
+
return execSync(command, { timeout: timeoutMs, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
4251
|
+
} catch {
|
|
4252
|
+
return null;
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
function isMacOSFocused(terminal) {
|
|
4256
|
+
const frontApp = execWithTimeout(`osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true'`);
|
|
4257
|
+
if (!frontApp)
|
|
4258
|
+
return false;
|
|
4259
|
+
return terminal.macProcessNames.some((name) => name.toLowerCase() === frontApp.toLowerCase());
|
|
4260
|
+
}
|
|
4261
|
+
function isPidAncestorOfCurrentProcess(pid) {
|
|
4262
|
+
let currentPid = process.pid;
|
|
4263
|
+
for (let depth = 0;depth < 20; depth++) {
|
|
4264
|
+
if (currentPid === pid)
|
|
4265
|
+
return true;
|
|
4266
|
+
if (currentPid <= 1)
|
|
4267
|
+
return false;
|
|
4268
|
+
try {
|
|
4269
|
+
const stat = readFileSync2(`/proc/${currentPid}/stat`, "utf-8");
|
|
4270
|
+
const match = stat.match(/^\d+\s+\(.*?\)\s+\S+\s+(\d+)/);
|
|
4271
|
+
if (!match)
|
|
4272
|
+
return false;
|
|
4273
|
+
currentPid = parseInt(match[1], 10);
|
|
4274
|
+
} catch {
|
|
4275
|
+
return false;
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
return false;
|
|
4279
|
+
}
|
|
4280
|
+
function isLinuxX11Focused() {
|
|
4281
|
+
const pidStr = execWithTimeout("xdotool getactivewindow getwindowpid");
|
|
4282
|
+
if (!pidStr)
|
|
4283
|
+
return false;
|
|
4284
|
+
const pid = parseInt(pidStr, 10);
|
|
4285
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
4286
|
+
return false;
|
|
4287
|
+
return isPidAncestorOfCurrentProcess(pid);
|
|
4288
|
+
}
|
|
4289
|
+
function isHyprlandFocused() {
|
|
4290
|
+
const output = execWithTimeout("hyprctl activewindow -j");
|
|
4291
|
+
if (!output)
|
|
4292
|
+
return false;
|
|
4293
|
+
try {
|
|
4294
|
+
const data = JSON.parse(output);
|
|
4295
|
+
const pid = data?.pid;
|
|
4296
|
+
if (typeof pid !== "number" || pid <= 0)
|
|
4297
|
+
return false;
|
|
4298
|
+
return isPidAncestorOfCurrentProcess(pid);
|
|
4299
|
+
} catch {
|
|
4300
|
+
return false;
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
function isSwayFocused() {
|
|
4304
|
+
const output = execWithTimeout("swaymsg -t get_tree", 1000);
|
|
4305
|
+
if (!output)
|
|
4306
|
+
return false;
|
|
4307
|
+
try {
|
|
4308
|
+
const tree = JSON.parse(output);
|
|
4309
|
+
const pid = findFocusedPid(tree);
|
|
4310
|
+
if (pid === null)
|
|
4311
|
+
return false;
|
|
4312
|
+
return isPidAncestorOfCurrentProcess(pid);
|
|
4313
|
+
} catch {
|
|
4314
|
+
return false;
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
function findFocusedPid(node) {
|
|
4318
|
+
if (node.focused === true && typeof node.pid === "number") {
|
|
4319
|
+
return node.pid;
|
|
4320
|
+
}
|
|
4321
|
+
if (Array.isArray(node.nodes)) {
|
|
4322
|
+
for (const child of node.nodes) {
|
|
4323
|
+
const pid = findFocusedPid(child);
|
|
4324
|
+
if (pid !== null)
|
|
4325
|
+
return pid;
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4328
|
+
if (Array.isArray(node.floating_nodes)) {
|
|
4329
|
+
for (const child of node.floating_nodes) {
|
|
4330
|
+
const pid = findFocusedPid(child);
|
|
4331
|
+
if (pid !== null)
|
|
4332
|
+
return pid;
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
return null;
|
|
4336
|
+
}
|
|
4337
|
+
function isKDEWaylandFocused() {
|
|
4338
|
+
const windowId = execWithTimeout("kdotool getactivewindow");
|
|
4339
|
+
if (!windowId)
|
|
4340
|
+
return false;
|
|
4341
|
+
const pidStr = execWithTimeout(`kdotool getwindowpid ${windowId}`);
|
|
4342
|
+
if (!pidStr)
|
|
4343
|
+
return false;
|
|
4344
|
+
const pid = parseInt(pidStr, 10);
|
|
4345
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
4346
|
+
return false;
|
|
4347
|
+
return isPidAncestorOfCurrentProcess(pid);
|
|
4348
|
+
}
|
|
4349
|
+
function isLinuxWaylandFocused() {
|
|
4350
|
+
const env = process.env;
|
|
4351
|
+
if (env.HYPRLAND_INSTANCE_SIGNATURE) {
|
|
4352
|
+
return isHyprlandFocused();
|
|
4353
|
+
}
|
|
4354
|
+
if (env.SWAYSOCK) {
|
|
4355
|
+
return isSwayFocused();
|
|
4356
|
+
}
|
|
4357
|
+
if (env.KDE_SESSION_VERSION) {
|
|
4358
|
+
return isKDEWaylandFocused();
|
|
4359
|
+
}
|
|
4360
|
+
return false;
|
|
4361
|
+
}
|
|
4362
|
+
function isWindowsFocused() {
|
|
4363
|
+
const script = `
|
|
4364
|
+
Add-Type @"
|
|
4365
|
+
using System;
|
|
4366
|
+
using System.Runtime.InteropServices;
|
|
4367
|
+
public class FocusHelper {
|
|
4368
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
4369
|
+
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
|
|
4370
|
+
}
|
|
4371
|
+
"@
|
|
4372
|
+
$hwnd = [FocusHelper]::GetForegroundWindow()
|
|
4373
|
+
$pid = 0
|
|
4374
|
+
[void][FocusHelper]::GetWindowThreadProcessId($hwnd, [ref]$pid)
|
|
4375
|
+
Write-Output $pid
|
|
4376
|
+
`.trim().replace(/\n/g, "; ");
|
|
4377
|
+
const pidStr = execWithTimeout(`powershell -NoProfile -Command "${script}"`, 1000);
|
|
4378
|
+
if (!pidStr)
|
|
4379
|
+
return false;
|
|
4380
|
+
const pid = parseInt(pidStr, 10);
|
|
4381
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
4382
|
+
return false;
|
|
4383
|
+
let currentPid = process.pid;
|
|
4384
|
+
for (let depth = 0;depth < 20; depth++) {
|
|
4385
|
+
if (currentPid === pid)
|
|
4386
|
+
return true;
|
|
4387
|
+
if (currentPid <= 1)
|
|
4388
|
+
return false;
|
|
4389
|
+
const parentPidStr = execWithTimeout(`powershell -NoProfile -Command "(Get-Process -Id ${currentPid}).Parent.Id"`, 500);
|
|
4390
|
+
if (!parentPidStr)
|
|
4391
|
+
return false;
|
|
4392
|
+
currentPid = parseInt(parentPidStr, 10);
|
|
4393
|
+
if (!Number.isFinite(currentPid))
|
|
4394
|
+
return false;
|
|
4395
|
+
}
|
|
4396
|
+
return false;
|
|
4397
|
+
}
|
|
4398
|
+
function isTerminalFocused() {
|
|
4399
|
+
try {
|
|
4400
|
+
const platform3 = process.platform;
|
|
4401
|
+
if (platform3 === "darwin") {
|
|
4402
|
+
const terminal = detectTerminal();
|
|
4403
|
+
if (!terminal || terminal.macProcessNames.length === 0)
|
|
4404
|
+
return false;
|
|
4405
|
+
return isMacOSFocused(terminal);
|
|
4406
|
+
}
|
|
4407
|
+
if (platform3 === "linux") {
|
|
4408
|
+
if (process.env.WAYLAND_DISPLAY) {
|
|
4409
|
+
return isLinuxWaylandFocused();
|
|
4410
|
+
}
|
|
4411
|
+
if (process.env.DISPLAY) {
|
|
4412
|
+
return isLinuxX11Focused();
|
|
4413
|
+
}
|
|
4414
|
+
return false;
|
|
4415
|
+
}
|
|
4416
|
+
if (platform3 === "win32") {
|
|
4417
|
+
return isWindowsFocused();
|
|
4418
|
+
}
|
|
4419
|
+
return false;
|
|
4420
|
+
} catch {
|
|
4421
|
+
return false;
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4201
4425
|
// src/index.ts
|
|
4202
4426
|
var IDLE_COMPLETE_DELAY_MS = 350;
|
|
4203
4427
|
var pendingIdleTimers = new Map;
|
|
@@ -4229,6 +4453,9 @@ function getNotificationTitle(config, projectName) {
|
|
|
4229
4453
|
return "OpenCode";
|
|
4230
4454
|
}
|
|
4231
4455
|
async function handleEvent(config, eventType, projectName, elapsedSeconds, sessionTitle) {
|
|
4456
|
+
if (config.suppressWhenFocused && isTerminalFocused()) {
|
|
4457
|
+
return;
|
|
4458
|
+
}
|
|
4232
4459
|
const promises = [];
|
|
4233
4460
|
const rawMessage = getMessage(config, eventType);
|
|
4234
4461
|
const message = interpolateMessage(rawMessage, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mohak34/opencode-notifier",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30-beta.0",
|
|
4
4
|
"description": "OpenCode plugin that sends system notifications and plays sounds when permission is needed, generation completes, or errors occur",
|
|
5
5
|
"author": "mohak34",
|
|
6
6
|
"license": "MIT",
|