Skip to content

Add external presenter support - #1147

Open
backnotprop wants to merge 5 commits into
mainfrom
feat/herdr-presenter
Open

Add external presenter support#1147
backnotprop wants to merge 5 commits into
mainfrom
feat/herdr-presenter

Conversation

@backnotprop

@backnotprop backnotprop commented Jul 28, 2026

Copy link
Copy Markdown
Owner

What changed

  • Add an optional external presenter for Plannotator's local review URL.
  • Use it across the shared server and Pi extension before opening the default browser.
  • Close the exact external presentation when its Plannotator session stops.
  • Keep the existing browser behavior when no presenter is configured or presentation fails.

Why

Terminal environments such as Herdr already have their own browser surface. This lets them display Plannotator's existing page there without moving approvals, feedback, or agent behavior out of Plannotator.

The presenter is a small executable configured in config.json. Plannotator sends it one JSON request to present the URL and another to dismiss the returned handle. Plannotator does not depend on Herdr.

Verification

  • 329 relevant server, hook, Pi, and presenter tests pass.
  • TypeScript checks pass across the workspace.
  • The companion Herdr plugin was proven end to end: configure, open the local page, return the pane handle, and dismiss that exact pane.

Companion plugin: https://github.com/plannotator/herdr-plannotator

Known residuals

  • The Pi extension keeps its module-global session latch (sessionAlive plus the browser-session start gate); restructuring it is out of scope here and the stale-ctx guard from Pi 0.82.1 stale-ctx guard crashes the extension after plan approval in print mode #1140 covers the teardown paths it does not.
  • A presenter present aborted mid-flight can leave its child running for up to the present timeout window (roughly 5-15s) before termination escalates; the exit-time SIGKILL bounds this to the lifetime of the CLI process and it is accepted for now.

Conflict resolutions:
- packages/server/annotate.ts: carried main's #1143 client-lease cleanup
  (clientLease.cancel() + closeSessions()) into the branch's memoized async
  stop closure, ahead of server.stop, keeping the presenter dismiss after
  server stop. A HEAD-favoring resolve would have regressed the
  abandoned-gate dismissal.
- apps/pi-extension/index.ts: kept main's sessionAlive latch (383ac97)
  assigned synchronously before the branch's awaited
  stopActivePlannotatorBrowserSessions(), so the stale-ctx guard is not
  delayed by the presenter dismiss budget.
- apps/pi-extension/plannotator-browser.ts: composed main's .catch +
  console.error on the fire-and-forget browser open with the branch's
  presenter presentation flow.
- apps/pi-extension/vendor.sh: union of both module lists (main's
  annotate-client-lease + annotate-decision, branch's presenter).

Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
Re-review follow-ups on the external presenter feature:

- annotate server keeps the force-stop, with a lease-stream flush first.
  Non-force was considered per review guidance, but the server runs with
  idleTimeout 0 and a connected UI holds heartbeat SSE streams (external
  annotations), so a graceful stop pends until the client disconnects
  while presenter hosts only close the client pane after the dismiss
  that runs once stop resolves. Bun also ignores stop(true) after a
  graceful stop has begun, ruling out graceful-then-force. A brief flush
  after clientLease.closeSessions() lets lease clients observe a clean
  end-of-stream instead of ECONNRESET (annotate.test.ts lease test).

- strict annotate gates publish the stdout decision record (and result
  file) BEFORE server cleanup, and a rejected cleanup routes to the
  documented environment-failure exit 2. Previously a stopServer()
  rejection exited with no record at all, which --require-approval
  callers would misread as "the reviewer did not approve".

- the shutdown coordinator's cleanup budget is now
  PRESENTER_DISMISS_TIMEOUT_MS + 2s so a timing-out dismiss resolves
  inside the budget instead of racing the force-exit, and presenter
  children are tracked and SIGKILLed synchronously on process exit —
  the unref'd SIGTERM->SIGKILL escalation (now 2s) could never fire
  before the CLI exited, leaving hung presenters orphaned.

- pi's session_shutdown catches (logs, never rethrows) failures from
  stopActivePlannotatorBrowserSessions(), which can reject with an
  AggregateError — same bug class as 383ac97.

Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
Adds the env var + config.json key to the repo env-var table (AGENTS.md,
the CLAUDE.md symlink source) and the marketing environment-variables
reference, plus a config-file note in getting-started/configuration.
Documents that PLANNOTATOR_SKIP_BROWSER_OPEN=1 wins over the presenter.

Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant