diff --git a/package-lock.json b/package-lock.json index 10abc82..bd384b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "opencode-for-devcontainers", - "version": "0.1.0", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opencode-for-devcontainers", - "version": "0.1.0", + "version": "0.1.3", "license": "MIT", "devDependencies": { "@types/node": "^20.11.0", diff --git a/package.json b/package.json index 22995b6..738c101 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "opencode-for-devcontainers", "displayName": "OpenCode for Dev Containers", "description": "Run OpenCode locally but execute commands within a devcontainer", - "version": "0.1.2", + "version": "0.1.3", "publisher": "marchingphoenix", "license": "MIT", "repository": { diff --git a/src/chat/chatParticipant.ts b/src/chat/chatParticipant.ts index fca49c2..18fb064 100644 --- a/src/chat/chatParticipant.ts +++ b/src/chat/chatParticipant.ts @@ -199,6 +199,7 @@ async function handleAsk( } completed = true; eventListener.dispose(); + stateListener.dispose(); cancelListener.dispose(); // Render subagent summary. @@ -223,6 +224,15 @@ async function handleAsk( } }); + // Safety net: if the bridge stops or errors without emitting an + // event (e.g. process killed externally), resolve the promise so + // the chat response doesn't hang indefinitely. + const stateListener = bridge.onStateChanged((state) => { + if (state === "stopped" || state === "error") { + finish({ error: "OpenCode process exited unexpectedly" }); + } + }); + const cancelListener = token.onCancellationRequested(() => { bridge.cancelCurrentRequest(); stream.markdown("\n_Request cancelled._\n"); diff --git a/src/chat/opencodeBridge.test.ts b/src/chat/opencodeBridge.test.ts index 0b7e075..f9067bb 100644 --- a/src/chat/opencodeBridge.test.ts +++ b/src/chat/opencodeBridge.test.ts @@ -301,6 +301,71 @@ describe("onStateChanged", () => { }); }); +// --------------------------------------------------------------------------- +// process exit — event notification +// --------------------------------------------------------------------------- + +describe("process exit", () => { + function getExitHandler() { + const exitCall = mockProcess.on.mock.calls.find( + ([event]: [string]) => event === "exit" + ); + return exitCall?.[1] as ((code: number | null) => void) | undefined; + } + + it("fires error event on clean exit (code 0)", async () => { + await bridge.start(); + const listener = vi.fn(); + bridge.onEvent(listener); + + const exitHandler = getExitHandler(); + expect(exitHandler).toBeDefined(); + exitHandler!(0); + + expect(listener).toHaveBeenCalledWith( + expect.objectContaining({ + type: "error", + message: expect.stringContaining("exited unexpectedly"), + }) + ); + expect(bridge.state).toBe("stopped"); + }); + + it("fires error event on clean exit (code null)", async () => { + await bridge.start(); + const listener = vi.fn(); + bridge.onEvent(listener); + + const exitHandler = getExitHandler(); + exitHandler!(null); + + expect(listener).toHaveBeenCalledWith( + expect.objectContaining({ + type: "error", + message: expect.stringContaining("exited unexpectedly"), + }) + ); + expect(bridge.state).toBe("stopped"); + }); + + it("fires error event on non-zero exit", async () => { + await bridge.start(); + const listener = vi.fn(); + bridge.onEvent(listener); + + const exitHandler = getExitHandler(); + exitHandler!(1); + + expect(listener).toHaveBeenCalledWith( + expect.objectContaining({ + type: "error", + message: expect.stringContaining("exited with code 1"), + }) + ); + expect(bridge.state).toBe("error"); + }); +}); + // --------------------------------------------------------------------------- // dispose // --------------------------------------------------------------------------- diff --git a/src/chat/opencodeBridge.ts b/src/chat/opencodeBridge.ts index 4beb633..33beb0f 100644 --- a/src/chat/opencodeBridge.ts +++ b/src/chat/opencodeBridge.ts @@ -276,6 +276,10 @@ export class OpenCodeBridge implements vscode.Disposable { }); this.setState("error"); } else { + this._onEvent.fire({ + type: "error", + message: "OpenCode process exited unexpectedly", + }); this.setState("stopped"); } }