feat(server): always surface the session URL on every start - #1184
feat(server): always surface the session URL on every start#1184technicalpickles wants to merge 2 commits into
Conversation
handleServerReady printed the session URL on three paths only: a remote session, the Codex desktop host, or a browser that failed to open. The common case, a local session whose browser opened fine, printed nothing, so a closed tab or a second machine left neither the user nor the agent driving the session any way back to it. Two changes, in the order the issue lays out: 1. One unconditional stderr line on every start, "Plannotator session ready: <url>". One line, URL last, stable format, because agents grep it. The remote and failed-launch branches now add context lines and never repeat the URL. The Codex desktop case existed solely to print the URL when the browser opened, so it and isCodexDesktopHost go with it. 2. PLANNOTATOR_READY_FILE now defaults to <dataDir>/ready.jsonl. The side channel already worked, but only amp and the OpenCode bridge set it; Claude Code spawns the CLI straight from a hook, so nothing set the variable and the metadata the server already publishes landed nowhere. apps/hook calls ensureReadyFileEnv() at startup, which leaves a host-supplied path alone, so tail -n 1 ready.jsonl is the newest session. The Pi runtime mirrors change 1 through its own surface: it owns the terminal, so openBrowserForServer notifies unconditionally instead of writing to stderr. Pi never publishes a ready file, so change 2 has no counterpart there. Also: the OpenCode stderr forwarder keeps forwarding the remote follow-up line under its new phrasing, ready.jsonl joins the uninstall purge list, and the env-var reference plus the troubleshooting "lost a tab" section document both recovery routes. Criterion 2 of the issue, a stable port across plan resubmissions, is deliberately not here. It is a separate PR. Refs backnotprop#1134
Review (at
|
Review follow-up on backnotprop#1184. Drop the always-on ready file. The default duplicated the session registry that already exists (`packages/server/sessions.ts`, surfaced by `plannotator sessions`) with strictly less information: no pid, no timestamp, no liveness. It also grew unboundedly on pure append, and the documented `tail -n 1` returned the last session *started* rather than the last one *alive*, which is exactly the stale-server confusion this branch set out to kill. The registry already answers the question, so only the default goes. `PLANNOTATOR_READY_FILE` stays as the opt-in host side channel and host-supplied values behave as before, so amp and OpenCode are unaffected. Make the format contract real. The assertions interpolated `SESSION_READY_LINE_PREFIX` into the expected string, so they compared the constant against itself: retexting it left every one of those tests green while `cli-bridge.ts` went on matching a hardcoded `/^Plannotator session ready\b/`, which means OpenCode users would have silently lost the URL with zero test failures. The two leading spaces in the emitted line were never asserted either. Now the literal bytes are pinned once, the emitted line is asserted including its indent and surrounding newlines, and a cross-component test in `cli-bridge.test.ts` feeds a line built from the exported constant through `formatUserFacingCliStderrLine`. Retexting the prefix fails 5 tests. Forward the browser-launch failure in OpenCode. "Could not open a browser automatically" matched none of the filter regexes, so the new output shape dropped the one line that explains a tab which never appeared. The old shape got forwarded; this was a regression introduced here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
This is a better review than I gave you a PR for. Both items are fixed, and you were right on the design call. 1, the format contract. Yeah, that one stings. Asserting against Pinned three ways now: the literal bytes, the emitted line asserted exactly with its two-space indent and newlines, and a cross-component test in Then I mutated the constant to see what happened. Five tests go red, the cross-component one included. Reverted. 2, the ready-file default is gone. You talked me out of it. So Now the part I didn't fix, before you find it yourself on re-review: the contract is pinned for the Bun runtime and OpenCode, and not for Pi. The dropped browser-failure line was ours, and it's fixed. "Could not open a browser automatically" matched none of the filter regexes, so OpenCode users got a URL and nothing about why no tab showed up, where the old shape forwarded it. Matched and covered. Body's fixed too. It advertised Marking it ready for review. |
Delta re-review (at
|
|
TLDR: This is the right design for the session URL line and the first real attempt at the core of #1134, but the delivery currently breaks both TUI hosts: OpenCode's default embedded runtime leaks the raw stderr print into its renderer, and Pi's two back-to-back notifies coalesce so the URL disappears exactly on the browser-failure path. Per-host prescription below. AI-assisted review. What's good, and should survive as-is: the single stable line with an exported SESSION_READY_LINE_PREFIX, URL last so agents can grep it; the cross-component drift test with its written rationale for hardcoding the literal; the docs pin; explicitly scoping to criterion 1 of #1134 and leaving the port half for a separate PR; and the stdout contract holds (we verified nothing lands on stdout in hook or json modes). Your CLI-bridge filter update for OpenCode is also correct. For the Bun CLI hosts (Claude Code, Codex, Gemini and friends) the unconditional un-TTY-gated stderr line is exactly right and should not change. The two host blockers, verified against both hosts' source:
Merge-forward items (not your fault, the branch is ~112 commits behind): shared-handlers now contains the QR, urlHost, and tailscale work inside the code this PR restructures, and post-merge the tailscale path would print the ready line twice. Whoever rebases should resolve the QR placement and add a single-print test across the tailscale path; the hook and json modes deserve a stdout-stays-clean test too. The core is sound and this closes a gap people have been asking about since #601. Rebase plus the two host fixes and it is mergeable. If you want, we can push the mechanical parts (the rebase and the OpenCode announce seam) to your branch and leave the Pi channel choice to you. |
Part of #1134, criterion 1 only. Leaving the stable-port half for a separate PR as you suggested, since the abandoned-session cleanup looked like its own conversation.
Summary
ctx.ui.notify, and the OpenCode stderr filter learned the new phrasing while still forwarding the older two-line shape from older binaries.isCodexDesktopHost()is gone. It existed only to special-case printing the URL, which is now unconditional.PLANNOTATOR_READY_FILEstays exactly what it was, an opt-in side channel that hosts point at their own temp file. No default, per your review. Discovery for everyone else is the stderr line plus the existingplannotator sessionsregistry, which is what the docs now say.The format is deliberately boring and stable, since the point is that agents grep it:
Two leading spaces, no trailing slash. Remote adds a port-forwarding line after it, and a failed browser launch adds a "could not open" line. The URL itself appears exactly once either way.
Test plan
bun run typecheckis clean.bun testhas four failures that predate this branch, and none of them are in code it touches. Three are inapps/pi-extension/server.test.ts, where a globalgpg.format=sshconfig leaks into the test's temp HOME and git can't write a commit object; that file imports only./server.tsplus the generated modules. The fourth is the git-timeout case inpackages/shared/workspace-status.test.ts, which lines up with the parallel-timing flakes you mentioned: this branch doesn't touchpackages/shared/at all, and that file passes 6/6 in isolation across three consecutive runs.New coverage in
packages/server/shared-handlers.test.tspins the URL appearing exactly once on the local, remote, and browser-failed paths, and pins the line's literal bytes including the two-space indent.apps/opencode-plugin/cli-bridge.test.tsimportsSESSION_READY_LINE_PREFIXand runs a line built from it through the real filter, so drifting the constant now breaks a test instead of silently costing OpenCode users the URL.