From d078244ea9250df5737966c5430f41897ac9d7f3 Mon Sep 17 00:00:00 2001 From: Diego Marinho <4332052+dmoliveira@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:59:52 +1100 Subject: [PATCH] fix: restore V1 sidebar and macOS process cleanup --- .github/workflows/ci.yml | 5 ++++ plugin/gateway-sidebar/dist/index.js | 6 ++-- plugin/gateway-sidebar/package-lock.json | 4 +-- plugin/gateway-sidebar/package.json | 4 +-- plugin/gateway-sidebar/src/index.tsx | 6 ++-- .../test/compatibility.test.mjs | 30 ++++++++++++++++++- scripts/ast_grep_installer.py | 14 +++++++-- tests/test_ast_grep_installer.py | 15 ++++++++++ 8 files changed, 71 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35c44f53..3bf8e969 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,11 @@ jobs: npm --prefix plugin/gateway-core run lint npm --prefix plugin/gateway-core run test + - name: Validate V1 execution sidebar + run: | + npm --prefix plugin/gateway-sidebar ci --ignore-scripts --no-audit --no-fund + npm --prefix plugin/gateway-sidebar test + - name: Install pinned OpenCode resume-test runtime run: | npm install --global opencode-ai@1.18.18 --no-audit --no-fund diff --git a/plugin/gateway-sidebar/dist/index.js b/plugin/gateway-sidebar/dist/index.js index 9e3140f9..be26d0d9 100644 --- a/plugin/gateway-sidebar/dist/index.js +++ b/plugin/gateway-sidebar/dist/index.js @@ -1,12 +1,12 @@ import { jsx as _jsx } from "@opentui/solid/jsx-runtime"; import { createExecutionStatusSidebar } from "./sidebar.js"; -const SUPPORTED_OPENCODE_VERSION = "1.18.18"; +const SUPPORTED_OPENCODE_VERSIONS = ["1.18.18", "1.18.33"]; const tui = async (api) => { - if (api.app.version !== SUPPORTED_OPENCODE_VERSION || typeof api.slots?.register !== "function") { + if (!SUPPORTED_OPENCODE_VERSIONS.includes(api.app.version) || typeof api.slots?.register !== "function") { api.ui.toast({ variant: "warning", title: "Execution sidebar disabled", - message: `Requires OpenCode ${SUPPORTED_OPENCODE_VERSION}; found ${api.app.version}.`, + message: `Requires OpenCode ${SUPPORTED_OPENCODE_VERSIONS.join(" or ")}; found ${api.app.version}.`, }); return; } diff --git a/plugin/gateway-sidebar/package-lock.json b/plugin/gateway-sidebar/package-lock.json index eedd7bbc..9aaf9f6a 100644 --- a/plugin/gateway-sidebar/package-lock.json +++ b/plugin/gateway-sidebar/package-lock.json @@ -16,10 +16,10 @@ "typescript": "5.8.2" }, "engines": { - "opencode": "1.18.18" + "opencode": "1.18.18 || 1.18.33" }, "peerDependencies": { - "@opencode-ai/plugin": "1.18.18", + "@opencode-ai/plugin": "1.18.18 || 1.18.33", "@opentui/core": "0.4.5", "@opentui/keymap": "0.4.5", "@opentui/solid": "0.4.5" diff --git a/plugin/gateway-sidebar/package.json b/plugin/gateway-sidebar/package.json index 8ada113a..5c9f3b71 100644 --- a/plugin/gateway-sidebar/package.json +++ b/plugin/gateway-sidebar/package.json @@ -7,7 +7,7 @@ "./tui": "./dist/index.js" }, "engines": { - "opencode": "1.18.18" + "opencode": "1.18.18 || 1.18.33" }, "scripts": { "build": "tsc -p tsconfig.json", @@ -15,7 +15,7 @@ "test": "npm run build && node --test test/**/*.test.mjs" }, "peerDependencies": { - "@opencode-ai/plugin": "1.18.18", + "@opencode-ai/plugin": "1.18.18 || 1.18.33", "@opentui/core": "0.4.5", "@opentui/keymap": "0.4.5", "@opentui/solid": "0.4.5" diff --git a/plugin/gateway-sidebar/src/index.tsx b/plugin/gateway-sidebar/src/index.tsx index 5383222f..baf6d796 100644 --- a/plugin/gateway-sidebar/src/index.tsx +++ b/plugin/gateway-sidebar/src/index.tsx @@ -3,14 +3,14 @@ import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui" import { createExecutionStatusSidebar } from "./sidebar.js" -const SUPPORTED_OPENCODE_VERSION = "1.18.18" +const SUPPORTED_OPENCODE_VERSIONS = ["1.18.18", "1.18.33"] const tui: TuiPlugin = async (api) => { - if (api.app.version !== SUPPORTED_OPENCODE_VERSION || typeof api.slots?.register !== "function") { + if (!SUPPORTED_OPENCODE_VERSIONS.includes(api.app.version) || typeof api.slots?.register !== "function") { api.ui.toast({ variant: "warning", title: "Execution sidebar disabled", - message: `Requires OpenCode ${SUPPORTED_OPENCODE_VERSION}; found ${api.app.version}.`, + message: `Requires OpenCode ${SUPPORTED_OPENCODE_VERSIONS.join(" or ")}; found ${api.app.version}.`, }) return } diff --git a/plugin/gateway-sidebar/test/compatibility.test.mjs b/plugin/gateway-sidebar/test/compatibility.test.mjs index 36584646..f774c8db 100644 --- a/plugin/gateway-sidebar/test/compatibility.test.mjs +++ b/plugin/gateway-sidebar/test/compatibility.test.mjs @@ -1,7 +1,9 @@ import assert from "node:assert/strict" -import { readFile } from "node:fs/promises" +import { mkdtemp, readFile, rm } from "node:fs/promises" +import { tmpdir } from "node:os" import { resolve } from "node:path" import test from "node:test" +import plugin from "../dist/index.js" const root = resolve(import.meta.dirname, "..") @@ -12,3 +14,29 @@ test("compiled sidebar module declares a TUI-only entrypoint", async () => { assert.doesNotMatch(source, /\bmcp\b/i) assert.doesNotMatch(source, /chat\.message/) }) + +test("verified V1 versions register the sidebar while unknown versions remain disabled", async () => { + const directory = await mkdtemp(resolve(tmpdir(), "sidebar-version-")) + try { + for (const version of ["1.18.18", "1.18.33", "1.18.34"]) { + const slots = [] + const warnings = [] + let dispose = () => {} + const api = { + app: { version }, + ui: { toast: (value) => warnings.push(value) }, + slots: { register: (value) => slots.push(value) }, + state: { path: { directory }, session: { get: () => undefined } }, + renderer: { requestRender() {} }, + event: { on: () => () => {} }, + lifecycle: { onDispose: (value) => { dispose = value } }, + } + await plugin.tui(api) + assert.equal(slots.length, version === "1.18.34" ? 0 : 1) + assert.equal(warnings.length, version === "1.18.34" ? 1 : 0) + dispose() + } + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) diff --git a/scripts/ast_grep_installer.py b/scripts/ast_grep_installer.py index c49cb444..8bd07cb9 100644 --- a/scripts/ast_grep_installer.py +++ b/scripts/ast_grep_installer.py @@ -1237,11 +1237,21 @@ def _signal_process_group(process_group: int, signal_number: int) -> None: def _wait_process_group_gone(process_group: int, timeout_seconds: float) -> bool: deadline = time.monotonic() + timeout_seconds - while _process_group_exists(process_group): + while True: + try: + exists = _process_group_exists(process_group) + except PermissionError: + # XNU can briefly return EPERM while a killed group is being reaped. + # Retry only within the existing bound; persistent errors still fail. + if sys.platform != "darwin" or time.monotonic() >= deadline: + raise + time.sleep(0.02) + continue + if not exists: + return True if time.monotonic() >= deadline: return False time.sleep(0.02) - return True def _terminate_process_group( diff --git a/tests/test_ast_grep_installer.py b/tests/test_ast_grep_installer.py index 98a2db59..81b921f2 100644 --- a/tests/test_ast_grep_installer.py +++ b/tests/test_ast_grep_installer.py @@ -678,6 +678,21 @@ def wait(self, timeout): ) settle.assert_called_once_with(process.pid, 1.0) + def test_postkill_wait_retries_transient_darwin_permission_error(self) -> None: + with mock.patch.object(installer.sys, "platform", "darwin"), mock.patch.object( + installer, "_process_group_exists", side_effect=[PermissionError, False] + ): + self.assertTrue(installer._wait_process_group_gone(40, 1.0)) + + def test_postkill_wait_rejects_persistent_or_non_darwin_permission_error(self) -> None: + for platform, timeout in (("darwin", 0), ("linux", 1.0)): + with self.subTest(platform=platform), mock.patch.object( + installer.sys, "platform", platform + ), mock.patch.object( + installer, "_process_group_exists", side_effect=PermissionError + ), self.assertRaises(PermissionError): + installer._wait_process_group_gone(40, timeout) + def test_binary_only_journal_recovers_without_download(self) -> None: archive_bytes, artifact = fixture_artifact() with tempfile.TemporaryDirectory() as raw: