diff --git a/packages/opencode/src/notification/index.ts b/packages/opencode/src/notification/index.ts index 163b88d6a795..ad4e339f2392 100644 --- a/packages/opencode/src/notification/index.ts +++ b/packages/opencode/src/notification/index.ts @@ -43,18 +43,37 @@ export function xml(title: string, message: string) { } export namespace Notification { - export async function terminalIsFocused(): Promise { - if (platform() !== "darwin") return false + export async function terminalIsFocused( + overridePlatform?: NodeJS.Platform, + ): Promise { + const os = overridePlatform ?? platform() - const result = await Process.text( - [ - "osascript", - "-e", - 'tell application "System Events" to get name of first application process whose frontmost is true', - ], - { nothrow: true }, - ) - return terminal(result.text.trim()) + if (os === "darwin") { + const result = await Process.text( + [ + "osascript", + "-e", + 'tell application "System Events" to get name of first application process whose frontmost is true', + ], + { nothrow: true }, + ) + return terminal(result.text.trim()) + } + + if (os === "linux") { + const result = await Process.text( + ["xdotool", "getactivewindow", "getwindowpid"], + { nothrow: true }, + ) + if (result.code !== 0) return true + const pid = parseInt(result.text.trim(), 10) + if (isNaN(pid)) return true + return pid === process.pid || pid === process.ppid + } + + if (os === "win32") return false + + return true } export async function show(title: string, message: string): Promise { diff --git a/packages/opencode/test/notification.test.ts b/packages/opencode/test/notification.test.ts index 07b8b600ee38..ff2214a90356 100644 --- a/packages/opencode/test/notification.test.ts +++ b/packages/opencode/test/notification.test.ts @@ -1,5 +1,6 @@ -import { describe, expect, test } from "bun:test" -import { terminal, xml } from "../src/notification" +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test" +import { terminal, xml, Notification } from "../src/notification" +import { Process } from "../src/util/process" describe("notification", () => { test("matches known terminal apps exactly after normalization", () => { @@ -15,3 +16,70 @@ describe("notification", () => { ) }) }) + +describe("terminalIsFocused", () => { + let origText: typeof Process.text + + beforeEach(() => { + origText = Process.text + }) + + afterEach(() => { + ;(Process as Record).text = origText + }) + + test("returns false on win32 (allow notifications since show() works)", async () => { + const result = await Notification.terminalIsFocused("win32") + expect(result).toBe(false) + }) + + test("returns true on unsupported platforms (freebsd)", async () => { + const result = await Notification.terminalIsFocused("freebsd") + expect(result).toBe(true) + }) + + test("linux: returns true when xdotool fails (not installed)", async () => { + ;(Process as Record).text = mock(async () => ({ + code: 1, + stdout: Buffer.alloc(0), + stderr: Buffer.from("command not found"), + text: "", + })) + const result = await Notification.terminalIsFocused("linux") + expect(result).toBe(true) + }) + + test("linux: returns true when xdotool returns matching PID", async () => { + ;(Process as Record).text = mock(async () => ({ + code: 0, + stdout: Buffer.from(String(process.pid) + "\n"), + stderr: Buffer.alloc(0), + text: String(process.pid) + "\n", + })) + const result = await Notification.terminalIsFocused("linux") + expect(result).toBe(true) + }) + + test("linux: returns false when xdotool returns non-matching PID", async () => { + const fakePid = 99999 + ;(Process as Record).text = mock(async () => ({ + code: 0, + stdout: Buffer.from(String(fakePid) + "\n"), + stderr: Buffer.alloc(0), + text: String(fakePid) + "\n", + })) + const result = await Notification.terminalIsFocused("linux") + expect(result).toBe(false) + }) + + test("linux: returns true when xdotool returns non-numeric output", async () => { + ;(Process as Record).text = mock(async () => ({ + code: 0, + stdout: Buffer.from("not-a-number\n"), + stderr: Buffer.alloc(0), + text: "not-a-number\n", + })) + const result = await Notification.terminalIsFocused("linux") + expect(result).toBe(true) + }) +})