From 011eb48ffd9884a08ec0c06c2a6b12e543c8cb69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20V=C4=83n=20T=C3=ADn?= <126467558+Sunmer8@users.noreply.github.com> Date: Sun, 1 Feb 2026 17:13:54 +0700 Subject: [PATCH] fix: improve Windows compatibility and fix event listener issues (#1102) Replace platform-specific 'which'/'where' commands with cross-platform Bun.which() API to fix Windows compatibility issues and simplify code. Fixes: - #1027: Comment-checker binary crashes on Windows (missing 'check' subcommand) - #1036: Session-notification listens to non-existent events - #1033: Infinite loop in session notifications - #599: Doctor incorrectly reports OpenCode as not installed on Windows - #1005: PowerShell path detection corruption on Windows Changes: - Use Bun.which() instead of spawning 'which'/'where' commands - Add 'check' subcommand to comment-checker invocation - Remove non-existent event listeners (session.updated, message.created) - Prevent notification commands from resetting their own state - Fix edge case: clear notifiedSessions if activity occurs during notification All changes are cross-platform compatible and tested on Windows/Linux/macOS. --- src/cli/doctor/checks/dependencies.ts | 8 +++----- src/cli/doctor/checks/opencode.ts | 13 +++---------- src/hooks/comment-checker/cli.ts | 2 +- src/hooks/session-notification-utils.ts | 22 +--------------------- src/hooks/session-notification.ts | 13 ++++++++++--- 5 files changed, 18 insertions(+), 40 deletions(-) diff --git a/src/cli/doctor/checks/dependencies.ts b/src/cli/doctor/checks/dependencies.ts index 09a476bcc..9b105812b 100644 --- a/src/cli/doctor/checks/dependencies.ts +++ b/src/cli/doctor/checks/dependencies.ts @@ -3,11 +3,9 @@ import { CHECK_IDS, CHECK_NAMES } from "../constants" async function checkBinaryExists(binary: string): Promise<{ exists: boolean; path: string | null }> { try { - const proc = Bun.spawn(["which", binary], { stdout: "pipe", stderr: "pipe" }) - const output = await new Response(proc.stdout).text() - await proc.exited - if (proc.exitCode === 0) { - return { exists: true, path: output.trim() } + const path = Bun.which(binary) + if (path) { + return { exists: true, path } } } catch { // intentionally empty - binary not found diff --git a/src/cli/doctor/checks/opencode.ts b/src/cli/doctor/checks/opencode.ts index dd1657a5f..e06ea4dec 100644 --- a/src/cli/doctor/checks/opencode.ts +++ b/src/cli/doctor/checks/opencode.ts @@ -55,16 +55,9 @@ export function buildVersionCommand( export async function findOpenCodeBinary(): Promise<{ binary: string; path: string } | null> { for (const binary of OPENCODE_BINARIES) { try { - const lookupCommand = getBinaryLookupCommand(process.platform) - const proc = Bun.spawn([lookupCommand, binary], { stdout: "pipe", stderr: "pipe" }) - const output = await new Response(proc.stdout).text() - await proc.exited - if (proc.exitCode === 0) { - const paths = parseBinaryPaths(output) - const selectedPath = selectBinaryPath(paths, process.platform) - if (selectedPath) { - return { binary, path: selectedPath } - } + const path = Bun.which(binary) + if (path) { + return { binary, path } } } catch { continue diff --git a/src/hooks/comment-checker/cli.ts b/src/hooks/comment-checker/cli.ts index 5ec5d4d9f..3026a939c 100644 --- a/src/hooks/comment-checker/cli.ts +++ b/src/hooks/comment-checker/cli.ts @@ -165,7 +165,7 @@ export async function runCommentChecker(input: HookInput, cliPath?: string, cust debugLog("running comment-checker with input:", jsonInput.substring(0, 200)) try { - const args = [binaryPath] + const args = [binaryPath, "check"] if (customPrompt) { args.push("--prompt", customPrompt) } diff --git a/src/hooks/session-notification-utils.ts b/src/hooks/session-notification-utils.ts index d59a4f362..81fce465b 100644 --- a/src/hooks/session-notification-utils.ts +++ b/src/hooks/session-notification-utils.ts @@ -3,28 +3,8 @@ import { spawn } from "bun" type Platform = "darwin" | "linux" | "win32" | "unsupported" async function findCommand(commandName: string): Promise { - 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 + return Bun.which(commandName) } catch { return null } diff --git a/src/hooks/session-notification.ts b/src/hooks/session-notification.ts index eded5181e..76b97dc9a 100644 --- a/src/hooks/session-notification.ts +++ b/src/hooks/session-notification.ts @@ -200,7 +200,9 @@ export function createSessionNotification( function markSessionActivity(sessionID: string) { cancelPendingNotification(sessionID) - notifiedSessions.delete(sessionID) + if (!executingNotifications.has(sessionID)) { + notifiedSessions.delete(sessionID) + } } async function executeNotification(sessionID: string, version: number) { @@ -254,6 +256,11 @@ export function createSessionNotification( } finally { executingNotifications.delete(sessionID) pendingTimers.delete(sessionID) + // Clear notified state if there was activity during notification + if (sessionActivitySinceIdle.has(sessionID)) { + notifiedSessions.delete(sessionID) + sessionActivitySinceIdle.delete(sessionID) + } } } @@ -262,7 +269,7 @@ export function createSessionNotification( const props = event.properties as Record | undefined - if (event.type === "session.updated" || event.type === "session.created") { + if (event.type === "session.created") { const info = props?.info as Record | undefined const sessionID = info?.id as string | undefined if (sessionID) { @@ -299,7 +306,7 @@ export function createSessionNotification( return } - if (event.type === "message.updated" || event.type === "message.created") { + if (event.type === "message.updated") { const info = props?.info as Record | undefined const sessionID = info?.sessionID as string | undefined if (sessionID) {