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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions plugin/gateway-sidebar/dist/index.js
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions plugin/gateway-sidebar/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions plugin/gateway-sidebar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
"./tui": "./dist/index.js"
},
"engines": {
"opencode": "1.18.18"
"opencode": "1.18.18 || 1.18.33"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"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"
Expand Down
6 changes: 3 additions & 3 deletions plugin/gateway-sidebar/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
30 changes: 29 additions & 1 deletion plugin/gateway-sidebar/test/compatibility.test.mjs
Original file line number Diff line number Diff line change
@@ -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, "..")

Expand All @@ -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 })
}
})
14 changes: 12 additions & 2 deletions scripts/ast_grep_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 15 additions & 0 deletions tests/test_ast_grep_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading