@mohak34/opencode-notifier 0.1.36 → 0.1.37-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 +3 -2
- package/dist/index.js +72 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -320,15 +320,16 @@ To disable this and always get notified:
|
|
|
320
320
|
| macOS | AppleScript (`System Events`) | None | Untested |
|
|
321
321
|
| Linux X11 | `xdotool` | `xdotool` installed | Untested |
|
|
322
322
|
| Linux Wayland (Hyprland) | `hyprctl activewindow` | None | Tested |
|
|
323
|
+
| Linux Wayland (Niri) | `niri msg --json focused-window` | None | Tested |
|
|
323
324
|
| Linux Wayland (Sway) | `swaymsg -t get_tree` | None | Untested |
|
|
324
325
|
| Linux Wayland (KDE) | `kdotool` | `kdotool` installed | Untested |
|
|
325
326
|
| Linux Wayland (GNOME) | Not supported | - | Falls back to always notifying |
|
|
326
|
-
| Linux Wayland (
|
|
327
|
+
| Linux Wayland (river, dwl, Cosmic, etc.) | Not supported | - | Falls back to always notifying |
|
|
327
328
|
| Windows | `GetForegroundWindow()` via PowerShell | None | Untested |
|
|
328
329
|
|
|
329
330
|
**Unsupported compositors**: Wayland has no standard protocol for querying the focused window. Each compositor has its own IPC, and GNOME intentionally doesn't expose focus information. Unsupported compositors fall back to always notifying.
|
|
330
331
|
|
|
331
|
-
**tmux/screen**: When running inside tmux,
|
|
332
|
+
**tmux/screen**: When running inside tmux, focus detection uses tmux pane state (`session_attached`, `window_active`, `pane_active`) via `tmux display-message`. This keeps suppression accurate when switching panes/windows/sessions. GNU Screen is not currently handled (falls back to always notifying).
|
|
332
333
|
|
|
333
334
|
**Fail-open design**: If detection fails for any reason (missing tools, unknown compositor, permissions), it falls back to always notifying. It never silently eats your notifications.
|
|
334
335
|
|
package/dist/index.js
CHANGED
|
@@ -250,6 +250,18 @@ if (platform === "Linux" || platform.match(/BSD$/)) {
|
|
|
250
250
|
var lastNotificationTime = {};
|
|
251
251
|
var lastLinuxNotificationId = null;
|
|
252
252
|
var linuxNotifySendSupportsReplace = null;
|
|
253
|
+
function sanitizeGhosttyField(value) {
|
|
254
|
+
return value.replace(/[;\x07\x1b\n\r]/g, "");
|
|
255
|
+
}
|
|
256
|
+
function formatGhosttyNotificationSequence(title, message, env = process.env) {
|
|
257
|
+
const escapedTitle = sanitizeGhosttyField(title);
|
|
258
|
+
const escapedMessage = sanitizeGhosttyField(message);
|
|
259
|
+
const payload = `\x1B]9;${escapedTitle}: ${escapedMessage}\x07`;
|
|
260
|
+
if (env.TMUX) {
|
|
261
|
+
return `\x1BPtmux;\x1B${payload}\x1B\\`;
|
|
262
|
+
}
|
|
263
|
+
return payload;
|
|
264
|
+
}
|
|
253
265
|
function detectNotifySendCapabilities() {
|
|
254
266
|
return new Promise((resolve) => {
|
|
255
267
|
execFile("notify-send", ["--version"], (error, stdout) => {
|
|
@@ -302,9 +314,8 @@ async function sendNotification(title, message, timeout, iconPath, notificationS
|
|
|
302
314
|
lastNotificationTime[message] = now;
|
|
303
315
|
if (notificationSystem === "ghostty") {
|
|
304
316
|
return new Promise((resolve) => {
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
process.stdout.write(`\x1B]9;${escapedTitle}: ${escapedMessage}\x07`, () => {
|
|
317
|
+
const sequence = formatGhosttyNotificationSequence(title, message);
|
|
318
|
+
process.stdout.write(sequence, () => {
|
|
308
319
|
resolve();
|
|
309
320
|
});
|
|
310
321
|
});
|
|
@@ -578,10 +589,23 @@ function getSwayActiveWindowId() {
|
|
|
578
589
|
return null;
|
|
579
590
|
}
|
|
580
591
|
}
|
|
592
|
+
function getNiriActiveWindowId() {
|
|
593
|
+
const output = execWithTimeout("niri msg --json focused-window", 1000);
|
|
594
|
+
if (!output)
|
|
595
|
+
return null;
|
|
596
|
+
try {
|
|
597
|
+
const data = JSON.parse(output);
|
|
598
|
+
return typeof data?.id === "number" ? String(data.id) : null;
|
|
599
|
+
} catch {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
581
603
|
function getLinuxWaylandActiveWindowId() {
|
|
582
604
|
const env = process.env;
|
|
583
605
|
if (env.HYPRLAND_INSTANCE_SIGNATURE)
|
|
584
606
|
return getHyprlandActiveWindowId();
|
|
607
|
+
if (env.NIRI_SOCKET)
|
|
608
|
+
return getNiriActiveWindowId();
|
|
585
609
|
if (env.SWAYSOCK)
|
|
586
610
|
return getSwayActiveWindowId();
|
|
587
611
|
if (env.KDE_SESSION_VERSION)
|
|
@@ -652,16 +676,19 @@ function getActiveWindowId() {
|
|
|
652
676
|
return null;
|
|
653
677
|
}
|
|
654
678
|
var cachedWindowId = getActiveWindowId();
|
|
655
|
-
|
|
656
|
-
function isTmuxPaneActive() {
|
|
679
|
+
function isTmuxPaneFocused(tmuxPane, probeResult) {
|
|
657
680
|
if (!tmuxPane)
|
|
658
|
-
return true;
|
|
659
|
-
const result = execWithTimeout(`tmux display-message -t ${tmuxPane} -p '#{session_attached} #{window_active} #{pane_active}'`);
|
|
660
|
-
if (!result)
|
|
661
681
|
return false;
|
|
662
|
-
|
|
682
|
+
if (!probeResult)
|
|
683
|
+
return false;
|
|
684
|
+
const [sessionAttached, windowActive, paneActive] = probeResult.split(" ");
|
|
663
685
|
return sessionAttached === "1" && windowActive === "1" && paneActive === "1";
|
|
664
686
|
}
|
|
687
|
+
function isTmuxPaneActive() {
|
|
688
|
+
const tmuxPane = process.env.TMUX_PANE ?? null;
|
|
689
|
+
const result = execFileWithTimeout("tmux", ["display-message", "-t", tmuxPane ?? "", "-p", "#{session_attached} #{window_active} #{pane_active}"]);
|
|
690
|
+
return isTmuxPaneFocused(tmuxPane, result);
|
|
691
|
+
}
|
|
665
692
|
function isTerminalFocused() {
|
|
666
693
|
try {
|
|
667
694
|
if (process.platform === "darwin") {
|
|
@@ -687,6 +714,34 @@ function isTerminalFocused() {
|
|
|
687
714
|
}
|
|
688
715
|
}
|
|
689
716
|
|
|
717
|
+
// src/permission-dedupe.ts
|
|
718
|
+
var PERMISSION_DEDUPE_WINDOW_MS = 1000;
|
|
719
|
+
var sessionLastPermissionAt = new Map;
|
|
720
|
+
var globalLastPermissionAt = 0;
|
|
721
|
+
function shouldSuppressPermissionAlert(sessionID, now = Date.now()) {
|
|
722
|
+
const sessionLastAt = sessionID ? sessionLastPermissionAt.get(sessionID) : undefined;
|
|
723
|
+
const latestSeen = Math.max(globalLastPermissionAt, sessionLastAt ?? 0);
|
|
724
|
+
const isDuplicate = latestSeen > 0 && now - latestSeen < PERMISSION_DEDUPE_WINDOW_MS;
|
|
725
|
+
if (isDuplicate) {
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
globalLastPermissionAt = now;
|
|
729
|
+
if (sessionID) {
|
|
730
|
+
sessionLastPermissionAt.set(sessionID, now);
|
|
731
|
+
}
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
function prunePermissionAlertState(cutoffMs) {
|
|
735
|
+
for (const [sessionID, timestamp] of sessionLastPermissionAt) {
|
|
736
|
+
if (timestamp < cutoffMs) {
|
|
737
|
+
sessionLastPermissionAt.delete(sessionID);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (globalLastPermissionAt < cutoffMs) {
|
|
741
|
+
globalLastPermissionAt = 0;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
690
745
|
// src/index.ts
|
|
691
746
|
var IDLE_COMPLETE_DELAY_MS = 350;
|
|
692
747
|
var pendingIdleTimers = new Map;
|
|
@@ -734,6 +789,7 @@ setInterval(() => {
|
|
|
734
789
|
sessionLastBusyAt.delete(sessionID);
|
|
735
790
|
}
|
|
736
791
|
}
|
|
792
|
+
prunePermissionAlertState(cutoff);
|
|
737
793
|
}, 5 * 60 * 1000);
|
|
738
794
|
function getNotificationTitle(config, projectName) {
|
|
739
795
|
if (config.showProjectName && projectName) {
|
|
@@ -924,7 +980,10 @@ var NotifierPlugin = async ({ client, directory }) => {
|
|
|
924
980
|
event: async ({ event }) => {
|
|
925
981
|
const config = getConfig();
|
|
926
982
|
if (event.type === "permission.asked") {
|
|
927
|
-
|
|
983
|
+
const sessionID = getSessionIDFromEvent(event);
|
|
984
|
+
if (!shouldSuppressPermissionAlert(sessionID)) {
|
|
985
|
+
await handleEventWithElapsedTime(client, config, "permission", projectName, event);
|
|
986
|
+
}
|
|
928
987
|
}
|
|
929
988
|
if (event.type === "session.idle") {
|
|
930
989
|
const sessionID = getSessionIDFromEvent(event);
|
|
@@ -951,7 +1010,9 @@ var NotifierPlugin = async ({ client, directory }) => {
|
|
|
951
1010
|
},
|
|
952
1011
|
"permission.ask": async () => {
|
|
953
1012
|
const config = getConfig();
|
|
954
|
-
|
|
1013
|
+
if (!shouldSuppressPermissionAlert(null)) {
|
|
1014
|
+
await handleEvent(config, "permission", projectName, null);
|
|
1015
|
+
}
|
|
955
1016
|
},
|
|
956
1017
|
"tool.execute.before": async (input) => {
|
|
957
1018
|
const config = getConfig();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mohak34/opencode-notifier",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37-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",
|