fix: check command existence before calling notify-send (#264)

This commit is contained in:
Sisyphus
2025-12-27 17:17:13 +09:00
committed by GitHub
parent c5205e7e2f
commit dec4994fd6
2 changed files with 186 additions and 12 deletions

View 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(() => {})
}
}

View File

@@ -1,6 +1,15 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { platform } from "os"
import { subagentSessions, getMainSessionID } from "../features/claude-code-session-state"
import {
getOsascriptPath,
getNotifySendPath,
getPowershellPath,
getAfplayPath,
getPaplayPath,
getAplayPath,
startBackgroundCheck,
} from "./session-notification-utils"
interface Todo {
content: string
@@ -51,15 +60,25 @@ async function sendNotification(
): Promise<void> {
switch (p) {
case "darwin": {
const osascriptPath = await getOsascriptPath()
if (!osascriptPath) return
const esTitle = title.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
}
case "linux":
await ctx.$`notify-send ${title} ${message} 2>/dev/null`.catch(() => {})
case "linux": {
const notifySendPath = await getNotifySendPath()
if (!notifySendPath) return
await ctx.$`${notifySendPath} ${title} ${message} 2>/dev/null`.catch(() => {})
break
}
case "win32": {
const powershellPath = await getPowershellPath()
if (!powershellPath) return
const psTitle = title.replace(/'/g, "''")
const psMessage = message.replace(/'/g, "''")
const toastScript = `
@@ -74,7 +93,7 @@ $Toast = [Windows.UI.Notifications.ToastNotification]::new($SerializedXml)
$Notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('OpenCode')
$Notifier.Show($Toast)
`.trim().replace(/\n/g, "; ")
await ctx.$`powershell -Command ${toastScript}`.catch(() => {})
await ctx.$`${powershellPath} -Command ${toastScript}`.catch(() => {})
break
}
}
@@ -82,17 +101,30 @@ $Notifier.Show($Toast)
async function playSound(ctx: PluginInput, p: Platform, soundPath: string): Promise<void> {
switch (p) {
case "darwin":
ctx.$`afplay ${soundPath}`.catch(() => {})
case "darwin": {
const afplayPath = await getAfplayPath()
if (!afplayPath) return
ctx.$`${afplayPath} ${soundPath}`.catch(() => {})
break
case "linux":
ctx.$`paplay ${soundPath} 2>/dev/null`.catch(() => {
ctx.$`aplay ${soundPath} 2>/dev/null`.catch(() => {})
})
}
case "linux": {
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
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
}
}
}
@@ -114,6 +146,8 @@ export function createSessionNotification(
const currentPlatform = detectPlatform()
const defaultSoundPath = getDefaultSoundPath(currentPlatform)
startBackgroundCheck(currentPlatform)
const mergedConfig = {
title: "OpenCode",
message: "Agent is ready for input",