@@ -5,8 +5,8 @@ import { type rpc } from "./worker"
55import path from "path"
66import { fileURLToPath } from "url"
77import { UI } from "@/cli/ui"
8- import { iife } from "@/util/iife"
98import { Log } from "@/util/log"
9+ import { withTimeout } from "@/util/timeout"
1010import { withNetworkOptions , resolveNetworkOptions } from "@/cli/network"
1111import { Filesystem } from "@/util/filesystem"
1212import type { Event } from "@opencode-ai/sdk/v2"
@@ -45,6 +45,20 @@ function createEventSource(client: RpcClient): EventSource {
4545 }
4646}
4747
48+ async function target ( ) {
49+ if ( typeof OPENCODE_WORKER_PATH !== "undefined" ) return OPENCODE_WORKER_PATH
50+ const dist = new URL ( "./cli/cmd/tui/worker.js" , import . meta. url )
51+ if ( await Filesystem . exists ( fileURLToPath ( dist ) ) ) return dist
52+ return new URL ( "./worker.ts" , import . meta. url )
53+ }
54+
55+ async function input ( value ?: string ) {
56+ const piped = process . stdin . isTTY ? undefined : await Bun . stdin . text ( )
57+ if ( ! value ) return piped
58+ if ( ! piped ) return value
59+ return piped + "\n" + value
60+ }
61+
4862export const TuiThreadCommand = cmd ( {
4963 command : "$0 [project]" ,
5064 describe : "start opencode tui" ,
@@ -97,100 +111,106 @@ export const TuiThreadCommand = cmd({
97111 }
98112
99113 // Resolve relative paths against PWD to preserve behavior when using --cwd flag
100- const baseCwd = process . env . PWD ?? process . cwd ( )
101- const cwd = args . project ? path . resolve ( baseCwd , args . project ) : process . cwd ( )
102- const localWorker = new URL ( "./worker.ts" , import . meta. url )
103- const distWorker = new URL ( "./cli/cmd/tui/worker.js" , import . meta. url )
104- const workerPath = await iife ( async ( ) => {
105- if ( typeof OPENCODE_WORKER_PATH !== "undefined" ) return OPENCODE_WORKER_PATH
106- if ( await Filesystem . exists ( fileURLToPath ( distWorker ) ) ) return distWorker
107- return localWorker
108- } )
114+ const root = process . env . PWD ?? process . cwd ( )
115+ const cwd = args . project ? path . resolve ( root , args . project ) : process . cwd ( )
116+ const file = await target ( )
109117 try {
110118 process . chdir ( cwd )
111- } catch ( e ) {
119+ } catch {
112120 UI . error ( "Failed to change directory to " + cwd )
113121 return
114122 }
115123
116- const worker = new Worker ( workerPath , {
124+ const worker = new Worker ( file , {
117125 env : Object . fromEntries (
118126 Object . entries ( process . env ) . filter ( ( entry ) : entry is [ string , string ] => entry [ 1 ] !== undefined ) ,
119127 ) ,
120128 } )
121129 worker . onerror = ( e ) => {
122130 Log . Default . error ( e )
123131 }
132+
124133 const client = Rpc . client < typeof rpc > ( worker )
125- process . on ( "uncaughtException" , ( e ) => {
126- Log . Default . error ( e )
127- } )
128- process . on ( "unhandledRejection" , ( e ) => {
134+ const error = ( e : unknown ) => {
129135 Log . Default . error ( e )
130- } )
131- process . on ( "SIGUSR2" , async ( ) => {
132- await client . call ( "reload" , undefined )
133- } )
136+ }
137+ const reload = ( ) => {
138+ client . call ( "reload" , undefined ) . catch ( ( err ) => {
139+ Log . Default . warn ( "worker reload failed" , {
140+ error : err instanceof Error ? err . message : String ( err ) ,
141+ } )
142+ } )
143+ }
144+ process . on ( "uncaughtException" , error )
145+ process . on ( "unhandledRejection" , error )
146+ process . on ( "SIGUSR2" , reload )
134147
135- const prompt = await iife ( async ( ) => {
136- const piped = ! process . stdin . isTTY ? await Bun . stdin . text ( ) : undefined
137- if ( ! args . prompt ) return piped
138- return piped ? piped + "\n" + args . prompt : args . prompt
139- } )
148+ let stopped = false
149+ const stop = async ( ) => {
150+ if ( stopped ) return
151+ stopped = true
152+ process . off ( "uncaughtException" , error )
153+ process . off ( "unhandledRejection" , error )
154+ process . off ( "SIGUSR2" , reload )
155+ await withTimeout ( client . call ( "shutdown" , undefined ) , 5000 ) . catch ( ( error ) => {
156+ Log . Default . warn ( "worker shutdown failed" , {
157+ error : error instanceof Error ? error . message : String ( error ) ,
158+ } )
159+ } )
160+ worker . terminate ( )
161+ }
162+
163+ const prompt = await input ( args . prompt )
140164 const config = await Instance . provide ( {
141165 directory : cwd ,
142166 fn : ( ) => TuiConfig . get ( ) ,
143167 } )
144168
145- // Check if server should be started (port or hostname explicitly set in CLI or config)
146- const networkOpts = await resolveNetworkOptions ( args )
147- const shouldStartServer =
169+ const network = await resolveNetworkOptions ( args )
170+ const external =
148171 process . argv . includes ( "--port" ) ||
149172 process . argv . includes ( "--hostname" ) ||
150173 process . argv . includes ( "--mdns" ) ||
151- networkOpts . mdns ||
152- networkOpts . port !== 0 ||
153- networkOpts . hostname !== "127.0.0.1"
154-
155- let url : string
156- let customFetch : typeof fetch | undefined
157- let events : EventSource | undefined
158-
159- if ( shouldStartServer ) {
160- // Start HTTP server for external access
161- const server = await client . call ( "server" , networkOpts )
162- url = server . url
163- } else {
164- // Use direct RPC communication (no HTTP)
165- url = "http://opencode.internal"
166- customFetch = createWorkerFetch ( client )
167- events = createEventSource ( client )
168- }
174+ network . mdns ||
175+ network . port !== 0 ||
176+ network . hostname !== "127.0.0.1"
169177
170- const tuiPromise = tui ( {
171- url,
172- config,
173- directory : cwd ,
174- fetch : customFetch ,
175- events,
176- args : {
177- continue : args . continue ,
178- sessionID : args . session ,
179- agent : args . agent ,
180- model : args . model ,
181- prompt,
182- fork : args . fork ,
183- } ,
184- onExit : async ( ) => {
185- await client . call ( "shutdown" , undefined )
186- } ,
187- } )
178+ const transport = external
179+ ? {
180+ url : ( await client . call ( "server" , network ) ) . url ,
181+ fetch : undefined ,
182+ events : undefined ,
183+ }
184+ : {
185+ url : "http://opencode.internal" ,
186+ fetch : createWorkerFetch ( client ) ,
187+ events : createEventSource ( client ) ,
188+ }
188189
189190 setTimeout ( ( ) => {
190191 client . call ( "checkUpgrade" , { directory : cwd } ) . catch ( ( ) => { } )
191- } , 1000 )
192+ } , 1000 ) . unref ?. ( )
192193
193- await tuiPromise
194+ try {
195+ await tui ( {
196+ url : transport . url ,
197+ config,
198+ directory : cwd ,
199+ fetch : transport . fetch ,
200+ events : transport . events ,
201+ args : {
202+ continue : args . continue ,
203+ sessionID : args . session ,
204+ agent : args . agent ,
205+ model : args . model ,
206+ prompt,
207+ fork : args . fork ,
208+ } ,
209+ onExit : stop ,
210+ } )
211+ } finally {
212+ await stop ( )
213+ }
194214 } finally {
195215 unguard ?.( )
196216 }
0 commit comments