11import type { PluginInput } from "@opencode-ai/plugin"
22import { platform } from "os"
3+ import { spawn } from "node:child_process"
34import { subagentSessions , getMainSessionID } from "../features/claude-code-session-state"
45import {
56 getOsascriptPath ,
@@ -11,6 +12,21 @@ import {
1112 startBackgroundCheck ,
1213} from "./session-notification-utils"
1314
15+ /**
16+ * Execute a command using node:child_process instead of Bun shell.
17+ * This avoids Bun's ShellInterpreter GC bug on Windows (oven-sh/bun#23177, #24368).
18+ */
19+ function execCommand ( command : string , args : string [ ] ) : Promise < void > {
20+ return new Promise ( ( resolve ) => {
21+ const proc = spawn ( command , args , {
22+ stdio : "ignore" ,
23+ detached : false ,
24+ } )
25+ proc . on ( "close" , ( ) => resolve ( ) )
26+ proc . on ( "error" , ( ) => resolve ( ) )
27+ } )
28+ }
29+
1430interface Todo {
1531 content : string
1632 status : string
@@ -65,14 +81,17 @@ async function sendNotification(
6581
6682 const esTitle = title . replace ( / \\ / g, "\\\\" ) . replace ( / " / g, '\\"' )
6783 const esMessage = message . replace ( / \\ / g, "\\\\" ) . replace ( / " / g, '\\"' )
68- await ctx . $ `${ osascriptPath } -e ${ "display notification \"" + esMessage + "\" with title \"" + esTitle + "\"" } ` . catch ( ( ) => { } )
84+ const script = `display notification "${ esMessage } " with title "${ esTitle } "`
85+ // Use node:child_process instead of Bun shell to avoid potential GC issues
86+ await execCommand ( osascriptPath , [ "-e" , script ] ) . catch ( ( ) => { } )
6987 break
7088 }
7189 case "linux" : {
7290 const notifySendPath = await getNotifySendPath ( )
7391 if ( ! notifySendPath ) return
7492
75- await ctx . $ `${ notifySendPath } ${ title } ${ message } 2>/dev/null` . catch ( ( ) => { } )
93+ // Use node:child_process instead of Bun shell to avoid potential GC issues
94+ await execCommand ( notifySendPath , [ title , message ] ) . catch ( ( ) => { } )
7695 break
7796 }
7897 case "win32" : {
@@ -93,7 +112,8 @@ $Toast = [Windows.UI.Notifications.ToastNotification]::new($SerializedXml)
93112$Notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('OpenCode')
94113$Notifier.Show($Toast)
95114` . trim ( ) . replace ( / \n / g, "; " )
96- await ctx . $ `${ powershellPath } -Command ${ toastScript } ` . catch ( ( ) => { } )
115+ // Use node:child_process instead of Bun shell to avoid GC crash (oven-sh/bun#23177)
116+ await execCommand ( powershellPath , [ "-Command" , toastScript ] ) . catch ( ( ) => { } )
97117 break
98118 }
99119 }
@@ -104,25 +124,29 @@ async function playSound(ctx: PluginInput, p: Platform, soundPath: string): Prom
104124 case "darwin" : {
105125 const afplayPath = await getAfplayPath ( )
106126 if ( ! afplayPath ) return
107- ctx . $ `${ afplayPath } ${ soundPath } ` . catch ( ( ) => { } )
127+ // Use node:child_process instead of Bun shell to avoid potential GC issues
128+ execCommand ( afplayPath , [ soundPath ] ) . catch ( ( ) => { } )
108129 break
109130 }
110131 case "linux" : {
111132 const paplayPath = await getPaplayPath ( )
112133 if ( paplayPath ) {
113- ctx . $ `${ paplayPath } ${ soundPath } 2>/dev/null` . catch ( ( ) => { } )
134+ // Use node:child_process instead of Bun shell to avoid potential GC issues
135+ execCommand ( paplayPath , [ soundPath ] ) . catch ( ( ) => { } )
114136 } else {
115137 const aplayPath = await getAplayPath ( )
116138 if ( aplayPath ) {
117- ctx . $ ` ${ aplayPath } ${ soundPath } 2>/dev/null` . catch ( ( ) => { } )
139+ execCommand ( aplayPath , [ soundPath ] ) . catch ( ( ) => { } )
118140 }
119141 }
120142 break
121143 }
122144 case "win32" : {
123145 const powershellPath = await getPowershellPath ( )
124146 if ( ! powershellPath ) return
125- ctx . $ `${ powershellPath } -Command ${ "(New-Object Media.SoundPlayer '" + soundPath + "').PlaySync()" } ` . catch ( ( ) => { } )
147+ // Use node:child_process instead of Bun shell to avoid GC crash (oven-sh/bun#23177)
148+ const soundScript = `(New-Object Media.SoundPlayer '${ soundPath . replace ( / ' / g, "''" ) } ').PlaySync()`
149+ execCommand ( powershellPath , [ "-Command" , soundScript ] ) . catch ( ( ) => { } )
126150 break
127151 }
128152 }
0 commit comments