Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/releases/v0.4.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- Failed initial state publication closes the desktop connection so a fresh connection can retry. Late readiness completion cannot resurrect a closed connection.
- Failed or ended session-event subscriptions reconnect with cancellable backoff, capped at five seconds.
- Cleanup after session deletion or movement remains retryable and does not block another session's cleanup.
- Shutdown intent survives failed cleanup. Physical-device aliases cannot bypass ownership or cause rollback to shut down another session's device.
- Shutdown intent survives failed cleanup when the retry omits the flag. Explicit `shutdown: false` releases the attachment without another power-off attempt. Physical-device aliases cannot bypass ownership or cause rollback to shut down another session's device.
- Disposal cancels pending discovery and recovery. Destroyed viewers cannot reconnect or retain decode timers.

Overlapping old tests were consolidated into transition tables, and the previous all-in-one desktop test was split into focused cases. Production function complexity is limited to nine. No runtime dependency or FSM framework was added.
Expand All @@ -33,6 +33,6 @@ Update the plugin pin in your global OpenCode configuration, preserving other se

Plugin exports and tool names are unchanged. Android still requires SDK Platform-Tools, Emulator, and a configured AVD. iOS requires Xcode and an iOS Simulator runtime on macOS. Both require Node 22.19+ on the 22.x line, or Node 24+, with npm.

Validation includes lint, formatting, typechecking, 296 passing tests and one existing Bun-only skip, rebuilt bundles, and installed-package loading. Each implementation PR passed Linux CI before merging; lifecycle suites also passed three consecutive local runs. Two fresh helper installations, native module loading, offline reuse, and stale-cache rejection passed on macOS arm64.
Validation includes lint, formatting, typechecking, 297 passing tests and one existing Bun-only skip, rebuilt bundles, and installed-package loading. Each implementation PR passed Linux CI before merging; lifecycle suites also passed three consecutive local runs. Two fresh helper installations, native module loading, offline reuse, and stale-cache rejection passed on macOS arm64.

Full changelog: https://github.com/viralcodex/opencode-demul/compare/v0.3.0...v0.4.0
2 changes: 1 addition & 1 deletion dist/gui.cjs

Large diffs are not rendered by default.

14 changes: 9 additions & 5 deletions src/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ test.each([false, true])(
},
);

test.each(["close", "reopen"])(
"failed cleanup quarantines the handle and retains shutdown intent through %s",
test.each(["close", "reopen", "detach"])(
"failed cleanup quarantines the handle and follows shutdown policy during %s",
async (recovery) => {
const item = fixture();
const first = await item.controller.open(caller, selection, "manual");
Expand All @@ -173,16 +173,20 @@ test.each(["close", "reopen"])(
item.controller.open({ ...caller, sessionID: "other" }, selection, "manual"),
).rejects.toMatchObject({ code: "device-busy" });
item.runtime.close = close;
if (recovery === "close") {
await item.controller.close(caller, { handle: first.handle }, "manual");
if (recovery !== "reopen") {
await item.controller.close(
caller,
{ handle: first.handle, ...(recovery === "detach" ? { shutdown: false } : {}) },
"manual",
);
expect(item.controller.state(caller)).toBeNull();
expect(item.calls).toEqual(["open", "close"]);
} else {
const next = await item.controller.open(caller, selection, "manual");
expect(next.handle.token).not.toBe(first.handle.token);
expect(item.calls).toEqual(["open", "close", "open"]);
}
expect(item.closes[0]?.shutdown).toBe(true);
expect(item.closes[0]?.shutdown).toBe(recovery === "detach" ? undefined : true);
await item.controller.dispose();
},
);
Expand Down
5 changes: 3 additions & 2 deletions src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,17 @@ export function createDeviceController(
const release = (
entry: Attachment,
reason: CleanupReason,
shutdown = false,
shutdown?: boolean,
signal?: AbortSignal,
): Promise<void> => {
if (entry.state.type === "released") return Promise.resolve();
if (entry.state.type === "cleanup") return entry.state.pending;
const previous = entry.state;
if (previous.type === "cleanup-failed") {
reason = previous.reason;
shutdown ||= previous.shutdown;
shutdown ??= previous.shutdown;
}
shutdown ??= false;
const pending = entry.queue.drain().then(async () => {
if (signal?.aborted) {
entry.state = previous;
Expand Down
Loading