fix: check command existence before calling notify-send (#264)
This commit is contained in:
140
src/hooks/session-notification-utils.ts
Normal file
140
src/hooks/session-notification-utils.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { spawn } from "bun"
|
||||||
|
|
||||||
|
type Platform = "darwin" | "linux" | "win32" | "unsupported"
|
||||||
|
|
||||||
|
let notifySendPath: string | null = null
|
||||||
|
let notifySendPromise: Promise<string | null> | null = null
|
||||||
|
|
||||||
|
let osascriptPath: string | null = null
|
||||||
|
let osascriptPromise: Promise<string | null> | null = null
|
||||||
|
|
||||||
|
let powershellPath: string | null = null
|
||||||
|
let powershellPromise: Promise<string | null> | null = null
|
||||||
|
|
||||||
|
let afplayPath: string | null = null
|
||||||
|
let afplayPromise: Promise<string | null> | null = null
|
||||||
|
|
||||||
|
let paplayPath: string | null = null
|
||||||
|
let paplayPromise: Promise<string | null> | null = null
|
||||||
|
|
||||||
|
let aplayPath: string | null = null
|
||||||
|
let aplayPromise: Promise<string | null> | null = null
|
||||||
|
|
||||||
|
async function findCommand(commandName: string): Promise<string | null> {
|
||||||
|
const isWindows = process.platform === "win32"
|
||||||
|
const cmd = isWindows ? "where" : "which"
|
||||||
|
|
||||||
|
try {
|
||||||
|
const proc = spawn([cmd, commandName], {
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
})
|
||||||
|
|
||||||
|
const exitCode = await proc.exited
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdout = await new Response(proc.stdout).text()
|
||||||
|
const path = stdout.trim().split("\n")[0]
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNotifySendPath(): Promise<string | null> {
|
||||||
|
if (notifySendPath !== null) return notifySendPath
|
||||||
|
if (notifySendPromise) return notifySendPromise
|
||||||
|
|
||||||
|
notifySendPromise = (async () => {
|
||||||
|
const path = await findCommand("notify-send")
|
||||||
|
notifySendPath = path
|
||||||
|
return path
|
||||||
|
})()
|
||||||
|
|
||||||
|
return notifySendPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOsascriptPath(): Promise<string | null> {
|
||||||
|
if (osascriptPath !== null) return osascriptPath
|
||||||
|
if (osascriptPromise) return osascriptPromise
|
||||||
|
|
||||||
|
osascriptPromise = (async () => {
|
||||||
|
const path = await findCommand("osascript")
|
||||||
|
osascriptPath = path
|
||||||
|
return path
|
||||||
|
})()
|
||||||
|
|
||||||
|
return osascriptPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPowershellPath(): Promise<string | null> {
|
||||||
|
if (powershellPath !== null) return powershellPath
|
||||||
|
if (powershellPromise) return powershellPromise
|
||||||
|
|
||||||
|
powershellPromise = (async () => {
|
||||||
|
const path = await findCommand("powershell")
|
||||||
|
powershellPath = path
|
||||||
|
return path
|
||||||
|
})()
|
||||||
|
|
||||||
|
return powershellPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAfplayPath(): Promise<string | null> {
|
||||||
|
if (afplayPath !== null) return afplayPath
|
||||||
|
if (afplayPromise) return afplayPromise
|
||||||
|
|
||||||
|
afplayPromise = (async () => {
|
||||||
|
const path = await findCommand("afplay")
|
||||||
|
afplayPath = path
|
||||||
|
return path
|
||||||
|
})()
|
||||||
|
|
||||||
|
return afplayPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPaplayPath(): Promise<string | null> {
|
||||||
|
if (paplayPath !== null) return paplayPath
|
||||||
|
if (paplayPromise) return paplayPromise
|
||||||
|
|
||||||
|
paplayPromise = (async () => {
|
||||||
|
const path = await findCommand("paplay")
|
||||||
|
paplayPath = path
|
||||||
|
return path
|
||||||
|
})()
|
||||||
|
|
||||||
|
return paplayPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAplayPath(): Promise<string | null> {
|
||||||
|
if (aplayPath !== null) return aplayPath
|
||||||
|
if (aplayPromise) return aplayPromise
|
||||||
|
|
||||||
|
aplayPromise = (async () => {
|
||||||
|
const path = await findCommand("aplay")
|
||||||
|
aplayPath = path
|
||||||
|
return path
|
||||||
|
})()
|
||||||
|
|
||||||
|
return aplayPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startBackgroundCheck(platform: Platform): void {
|
||||||
|
if (platform === "darwin") {
|
||||||
|
getOsascriptPath().catch(() => {})
|
||||||
|
getAfplayPath().catch(() => {})
|
||||||
|
} else if (platform === "linux") {
|
||||||
|
getNotifySendPath().catch(() => {})
|
||||||
|
getPaplayPath().catch(() => {})
|
||||||
|
getAplayPath().catch(() => {})
|
||||||
|
} else if (platform === "win32") {
|
||||||
|
getPowershellPath().catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,15 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import { platform } from "os"
|
import { platform } from "os"
|
||||||
import { subagentSessions, getMainSessionID } from "../features/claude-code-session-state"
|
import { subagentSessions, getMainSessionID } from "../features/claude-code-session-state"
|
||||||
|
import {
|
||||||
|
getOsascriptPath,
|
||||||
|
getNotifySendPath,
|
||||||
|
getPowershellPath,
|
||||||
|
getAfplayPath,
|
||||||
|
getPaplayPath,
|
||||||
|
getAplayPath,
|
||||||
|
startBackgroundCheck,
|
||||||
|
} from "./session-notification-utils"
|
||||||
|
|
||||||
interface Todo {
|
interface Todo {
|
||||||
content: string
|
content: string
|
||||||
@@ -51,15 +60,25 @@ async function sendNotification(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
switch (p) {
|
switch (p) {
|
||||||
case "darwin": {
|
case "darwin": {
|
||||||
|
const osascriptPath = await getOsascriptPath()
|
||||||
|
if (!osascriptPath) return
|
||||||
|
|
||||||
const esTitle = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
const esTitle = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
||||||
const esMessage = message.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
const esMessage = message.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
||||||
await ctx.$`osascript -e ${"display notification \"" + esMessage + "\" with title \"" + esTitle + "\""}`
|
await ctx.$`${osascriptPath} -e ${"display notification \"" + esMessage + "\" with title \"" + esTitle + "\""}`.catch(() => {})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "linux":
|
case "linux": {
|
||||||
await ctx.$`notify-send ${title} ${message} 2>/dev/null`.catch(() => {})
|
const notifySendPath = await getNotifySendPath()
|
||||||
|
if (!notifySendPath) return
|
||||||
|
|
||||||
|
await ctx.$`${notifySendPath} ${title} ${message} 2>/dev/null`.catch(() => {})
|
||||||
break
|
break
|
||||||
|
}
|
||||||
case "win32": {
|
case "win32": {
|
||||||
|
const powershellPath = await getPowershellPath()
|
||||||
|
if (!powershellPath) return
|
||||||
|
|
||||||
const psTitle = title.replace(/'/g, "''")
|
const psTitle = title.replace(/'/g, "''")
|
||||||
const psMessage = message.replace(/'/g, "''")
|
const psMessage = message.replace(/'/g, "''")
|
||||||
const toastScript = `
|
const toastScript = `
|
||||||
@@ -74,7 +93,7 @@ $Toast = [Windows.UI.Notifications.ToastNotification]::new($SerializedXml)
|
|||||||
$Notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('OpenCode')
|
$Notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('OpenCode')
|
||||||
$Notifier.Show($Toast)
|
$Notifier.Show($Toast)
|
||||||
`.trim().replace(/\n/g, "; ")
|
`.trim().replace(/\n/g, "; ")
|
||||||
await ctx.$`powershell -Command ${toastScript}`.catch(() => {})
|
await ctx.$`${powershellPath} -Command ${toastScript}`.catch(() => {})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,19 +101,32 @@ $Notifier.Show($Toast)
|
|||||||
|
|
||||||
async function playSound(ctx: PluginInput, p: Platform, soundPath: string): Promise<void> {
|
async function playSound(ctx: PluginInput, p: Platform, soundPath: string): Promise<void> {
|
||||||
switch (p) {
|
switch (p) {
|
||||||
case "darwin":
|
case "darwin": {
|
||||||
ctx.$`afplay ${soundPath}`.catch(() => {})
|
const afplayPath = await getAfplayPath()
|
||||||
|
if (!afplayPath) return
|
||||||
|
ctx.$`${afplayPath} ${soundPath}`.catch(() => {})
|
||||||
break
|
break
|
||||||
case "linux":
|
}
|
||||||
ctx.$`paplay ${soundPath} 2>/dev/null`.catch(() => {
|
case "linux": {
|
||||||
ctx.$`aplay ${soundPath} 2>/dev/null`.catch(() => {})
|
const paplayPath = await getPaplayPath()
|
||||||
})
|
if (paplayPath) {
|
||||||
|
ctx.$`${paplayPath} ${soundPath} 2>/dev/null`.catch(() => {})
|
||||||
|
} else {
|
||||||
|
const aplayPath = await getAplayPath()
|
||||||
|
if (aplayPath) {
|
||||||
|
ctx.$`${aplayPath} ${soundPath} 2>/dev/null`.catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case "win32":
|
}
|
||||||
ctx.$`powershell -Command ${"(New-Object Media.SoundPlayer '" + soundPath + "').PlaySync()"}`.catch(() => {})
|
case "win32": {
|
||||||
|
const powershellPath = await getPowershellPath()
|
||||||
|
if (!powershellPath) return
|
||||||
|
ctx.$`${powershellPath} -Command ${"(New-Object Media.SoundPlayer '" + soundPath + "').PlaySync()"}`.catch(() => {})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function hasIncompleteTodos(ctx: PluginInput, sessionID: string): Promise<boolean> {
|
async function hasIncompleteTodos(ctx: PluginInput, sessionID: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
@@ -114,6 +146,8 @@ export function createSessionNotification(
|
|||||||
const currentPlatform = detectPlatform()
|
const currentPlatform = detectPlatform()
|
||||||
const defaultSoundPath = getDefaultSoundPath(currentPlatform)
|
const defaultSoundPath = getDefaultSoundPath(currentPlatform)
|
||||||
|
|
||||||
|
startBackgroundCheck(currentPlatform)
|
||||||
|
|
||||||
const mergedConfig = {
|
const mergedConfig = {
|
||||||
title: "OpenCode",
|
title: "OpenCode",
|
||||||
message: "Agent is ready for input",
|
message: "Agent is ready for input",
|
||||||
|
|||||||
Reference in New Issue
Block a user