Files
backnotprop__plannotator/scripts/opencode2-native-commands-smoke.sh
Michael Ramos 7ee366d8a1 fix(opencode): show the session URL on OpenCode 2's native command path (#1435)
* fix(opencode): show the session URL on OpenCode 2's native command path

On OpenCode 2 a remote session's URL was invisible. runNativeCommand builds
its bridge client with createV2BridgeClient, which deliberately has no tui
domain, so toastPlannotatorUrl optional-chained to a no-op; both URL delivery
paths (the CLI stderr forwarder and the ready-file poller) route through it.
The V2 client's app.log is console.error, and OpenCode discards a server
plugin's stderr under both default launch modes (packages/cli/src/services/
standalone.ts uses stderr: "ignore" unless OPENCODE_PRINT_LOGS=1). Remote mode
also suppresses the browser, so /plannotator-review showed the user nothing at
all and presented as a hang.

Deliver the URL as a visible transcript notice instead. createSessionUrlNotifier
duck-types ctx.session.synthetic and exposes it to cli-bridge as notifyUrl, a
seam toastPlannotatorUrl prefers over the toast when present; OpenCode 1 clients
carry no notifyUrl and keep their real toast unchanged. The notice is posted
with resume: false, which upstream skips the wake for, so nothing starts a model
turn, and it carries the URL in both text and description because the TUI drops
a synthetic row whose description is empty and renders the description rather
than the text. Everything is guarded: a host without session.synthetic, or a
call with no session, gets no notifier and falls back to today's log-only
behavior, and a rejecting synthetic is caught and leaves the URL retryable by
the other delivery path.

The README's remedy line claimed remote sessions should read the URL from the
OpenCode log, which was never true; it now describes the transcript notice and
names OPENCODE_PRINT_LOGS=1 for older hosts.

Also fixes two bugs in the OpenCode 2 native-command smoke:

- scripts/opencode2-native-commands-smoke.sh looked for a node_modules/.bin/
  opencode binary. @opencode-ai/cli publishes opencode2 on every dist-tag, so
  the script failed before it started a server. It now tries both names and
  reports which it looked for.
- The command-ownership check read /api/command once, immediately after
  activation, racing the reclaim schedule whose last tick lands about 15.5s
  later. Under PLANNOTATOR_SMOKE_EXPECT_NATIVE=1 that reported a shadowing bug
  the reclaim had simply not reached yet. It now polls to a 30s deadline
  (PLANNOTATOR_SMOKE_COMMAND_TIMEOUT_MS), still only after /api/plugin reports
  the plugin loaded.

AI-assisted (Claude) under maintainer direction.

* fix(opencode): deliver the session URL on OpenCode 2's plan review path too

The first commit fixed only the native command path. The plan path builds its
own client (createV2Client, typed as { app: { agents, log } } with no notifier),
so a remote OpenCode 2 user who reached a review through submit_plan still never
saw the URL: no browser is opened for them and the plugin's console output is
discarded by the host.

The plan path now builds the same bridge client the command path uses, with
toolContext.sessionID, so it carries notifyUrl whenever the host exposes
session.synthetic. That covers both runtimes: the CLI runtime already prefers
notifyUrl inside toastPlannotatorUrl, and the embedded runtime's previously
empty logReady hook is now createPlanReadyNotifier.

That hook still does not log. app.log is console.error, the same stderr
handleServerReady already printed the URL to, so logging there would duplicate
the line in remote mode and add a stray one locally, which is why the hook was
empty. The transcript notice is a different surface, and it is the only one a
remote reviewer can see. Without session.synthetic the hook stays silent exactly
as before.

createV2Client is gone: it duplicated the bridge client's URL-deduped app.log
verbatim, and nothing else used it.

Three tests on the plan path (delivers the notice; stays silent and does not
re-log without synthetic; catches a rejecting notice) plus one that pins the two
wiring seams at source level, since the notifier tests all pass while the plan
path is wired to nothing, which is the shape the bug had.

Also from review: console.error is stubbed across the V2 URL delivery block, so
those tests no longer print URL lines into the suite output. The README bullet
now says the notice covers every way a session opens rather than slash commands
alone.

AI-assisted (Claude) under maintainer direction.
2026-08-31 13:12:23 -07:00

66 lines
2.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Verify the OpenCode 2 native slash commands against a host that actually has
# the post-#44765 command API.
#
# CI cannot do this: .github/workflows/test.yml pins @opencode-ai/cli to a
# `next` build, and `next` still ships the older command draft (no `add`), so
# the CI leg can only prove the fallback path. The command API currently lives
# on the `beta` and `dev` dist-tags, which move daily and are not something to
# pin a required check to. So this is a script a human runs before a release.
#
# Usage:
# scripts/opencode2-native-commands-smoke.sh [dist-tag] # default: dev
#
# What it proves:
# 1. The plugin activates without status:"failed".
# 2. All three slash commands resolve.
# 3. They resolve to the PLUGIN's definitions, not the markdown stubs the
# fixture installs into the sandbox config dir exactly as install.sh does.
# (3) is the shadowing check and is fatal here because of
# PLANNOTATOR_SMOKE_EXPECT_NATIVE=1.
#
# What it does NOT prove: that /plannotator-review opens the UI without a model
# turn. Run that by hand in the TUI against the same build.
set -euo pipefail
tag="${1:-dev}"
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
echo "==> installing @opencode-ai/cli@$tag into $work"
cd "$work"
npm init -y >/dev/null 2>&1
npm install --no-audit --no-fund "@opencode-ai/cli@$tag" >/dev/null
# @opencode-ai/cli publishes its binary as `opencode2` on every dist-tag
# (`latest`, `next`, `beta`, `dev`); only the separate `opencode-ai` package
# installs `opencode`. Looking for the wrong one failed this script before it
# ever started a server, so try both and say which names were checked.
opencode_bin=""
for candidate in opencode2 opencode; do
if [ -x "$work/node_modules/.bin/$candidate" ]; then
opencode_bin="$work/node_modules/.bin/$candidate"
break
fi
done
if [ -z "$opencode_bin" ]; then
echo "No OpenCode binary in $work/node_modules/.bin (looked for: opencode2, opencode)" >&2
ls -1 "$work/node_modules/.bin" >&2 || true
exit 1
fi
echo "==> using $opencode_bin"
"$opencode_bin" --version
echo "==> building and packing the plugin"
cd "$repo_root"
bun run build:opencode
cd "$repo_root/apps/opencode-plugin"
bun pm pack --filename "$work/plannotator-opencode.tgz" >/dev/null
echo "==> running the smoke with native commands required"
PLANNOTATOR_SMOKE_EXPECT_NATIVE=1 \
bun run --cwd "$repo_root/apps/opencode-plugin" smoke:v2 -- \
"$opencode_bin" \
"$work/plannotator-opencode.tgz"