diff --git a/packages/core/src/plugin/browser/index.ts b/packages/core/src/plugin/browser/index.ts index 1dffe3d7a64..d3eb80d8f9b 100644 --- a/packages/core/src/plugin/browser/index.ts +++ b/packages/core/src/plugin/browser/index.ts @@ -44,8 +44,10 @@ export default Plugin.define({ const browser = yield* Effect.acquireRelease( Effect.gen(function* () { const closed = yield* Deferred.make() - if (!active || browsers.has(input.sessionID)) - return yield* Effect.fail(call.error("unavailable", "Browser is unavailable.", {})) + if (!active) return yield* Effect.fail(call.error("unavailable", "Browser is unavailable.", {})) + // The newest desktop attachment wins so a re-register that races the + // previous connection's teardown does not leave the session detached. + yield* close(input.sessionID) const browser: Attachment = { connectionID: input.connectionID, state: null, diff --git a/packages/sdk/test/browser-plugin.test.ts b/packages/sdk/test/browser-plugin.test.ts index 0c129aeb440..159d1cbe298 100644 --- a/packages/sdk/test/browser-plugin.test.ts +++ b/packages/sdk/test/browser-plugin.test.ts @@ -113,10 +113,13 @@ test( expect(yield* host.execute({ type: "open" }).pipe(Effect.flip)).toMatchObject({ message: "No desktop browser is connected.", }) + const stale = yield* host.attach("stale") + // A newer attachment for the same session replaces the previous one. const attached = yield* host.attach("first") - expect( - yield* host.rpc.attach({ ...attached.input, connectionID: "duplicate" }, options).pipe(Effect.flip), - ).toMatchObject({ type: "unavailable" }) + yield* Fiber.join(stale.lifetime).pipe(Effect.timeout("5 seconds")) + expect(yield* host.rpc.state({ ...stale.input, state }, options).pipe(Effect.flip)).toMatchObject({ + type: "unavailable", + }) const other = Location.Ref.make({ directory: AbsolutePath.make(path.join(host.location.directory, "other")) }) yield* Effect.promise(() => mkdir(other.directory)) yield* host.opencode.plugin.list({ location: other })