pi-tern
Tern integration for the pi coding agent: TSP handshake, native Mermaid diagrams, WKWebView browser automation, pane capture and a session mirror.
Package details
Install pi-tern from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-tern- Package
pi-tern- Version
1.1.11- Published
- Oct 6, 2026
- Downloads
- 1,677/mo · 416/wk
- Author
- ghost-9
- License
- MIT
- Types
- extension
- Size
- 639.9 KB
- Dependencies
- 0 dependencies · 0 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-tern
Tern integration for the pi coding agent.
pi is a terminal coding agent. Tern is a terminal by Stencil Labs that renders native UI for programs that speak the Tern Surface Protocol (TSP): Mermaid diagrams, a WKWebView browser, native session surfaces. Programs that do not speak TSP are plain terminal panes.
This extension makes pi Tern-aware without patching pi:
- runs the TSP
hellohandshake, so pi knows it is inside Tern and what Tern supports; - renders Mermaid through Tern's built-in engine (merman) in a file block;
- drives Tern's built-in browser as a picture-in-picture and captures PNGs the model can see;
- captures panes and mirrors the conversation into a Markdown block;
- keeps the tab title live (
π <model> · <context> · <dir>) and can ring Tern's attention bell.
The extension itself never writes TSP frames, so pi's renderer is not disturbed — that is a deliberate boundary, since pi-tui's frames are multi-write synchronized-output transactions and a second writer tears them.
Native surfaces are a separate opt-in path, shipped in v1.0.0 and reached through the pi-tern launcher rather than the extension: the launcher probes Tern, and only when Tern has confirmed it is in an agent block does it run pi through a loader hook that hands rendering to Tern. Native mode is the special case, not the default — in a shell block, outside Tern, or with -p / --mode json|rpc, it runs stock pi. See Native surfaces need an agent block and docs/NATIVE-FINDINGS.md.
Features
| Feature | Status | What it does |
|---|---|---|
| TSP handshake | stable | Sends hello + DA1 on the pty and reads the reply through pi's raw input. Verified against Tern 0.5.1, whose reply advertises 44 node kinds and 10 features. That is Tern's vocabulary, not pi-tern's usage — pi-tern gates on three of them (image, chart, aside) and emits six node kinds |
| Mermaid diagrams | stable | tern_diagram / /tern diagram opens a file block rendered by Tern's merman engine (18 diagram types, including xychart-beta bar/line charts). --last uses the newest fence in the conversation; pin keeps one path so re-renders update the same block |
| Charts | stable | tern_chart draws bar/hbar, line, area, pie and donut from plain data as a themed SVG that Tern renders as a native image block. png: true rasterizes it locally so the model can read the chart back. Value labels keep full precision |
| Fleet panes | stable | tern_fleet treats a Tern pane as a thread: spawn (a pi -p task, an interactive pi, or any command) · list with liveness · send to steer · read · wait · stop · prune. Task text goes through a file, so prompts cannot break the shell. Panes are spawned with --keep-open so a finished task's output survives; prune closes those that exited and have been idle past a floor (default 30m), reporting why it kept each one it did not touch. dryRun: true first |
| Git worktrees | stable | tern_worktree list/create/remove/prune/status with dirty counts and optional startFromOrigin. Pure git — works with no Tern at all |
| Pull requests | stable | tern_pr summary (checks collapsed to counts plus failing names, plus a one-line verdict), list, comments, watch (gh pr checks --watch in a visible pane), open in Tern's browser |
| Capability contract | stable | tern_status { manifest: true } / /tern capabilities: one machine-readable document listing every capability, whether it is available here, and why not |
| Runnable shell blocks | stable | tern_run / /tern run [--last] runs a bash/sh command from the conversation in a visible Tern pane, waits for exit and returns the captured output (PI_TERN_RUN=0 disables) |
| Session mirror | stable | /tern mirror on writes this conversation to session-mirror.md and opens it as a Tern block; a session TOC, timestamps and tool lines are included, and Mermaid inside it renders natively |
| Data plane | stable | tern_db (SQLite read-only by default), tern_doc (live documents, unsaved edits, heading-aware edits), tern_board (native task board lanes/cards) through Tern's own window APIs via the pi-bridge mailbox |
| Carly integration | stable | tern_carly + /tern ask|remember|recall|schedule|tasks|cancel; a pi_tern() export lets Carly read pi's status. Carly uses remote providers: never send vault/secret material |
| Notebooks & settings | experimental | tern_notebook read <pane> for open notebook blocks (execution is not exposed by Tern's plugin API); tern_settings get|list|describe |
| Tern browser | stable | tern_browser drives Tern's WKWebView picture-in-picture: open, state, snapshot, act, eval, capture (returns an image), input, goto, nav, events, close |
| Pane inspection | stable | tern_capture (text, ANSI, HTML, scrollback, surfaces), tern_panes, tern_ctl |
| Pane events | stable | tern_watch waits for daemon events (pane_exited, …) with an optional pane filter, so pi can wait for a test run or server instead of polling |
| Pane output waits | stable | tern_watch --expect <regex> polls a pane until it prints a pattern (dev servers, builds) and returns the tail |
| Browser suite | stable | relay/CLI ops, stale-ref recovery, named PNG baselines, network capture (Resource Timing HAR-lite), PDF export, multi-tab listing/close, form-field helper |
| pi-bridge canvas | experimental | linked automatically on the first Tern session — installing pi-tern is enough; renders a native Markdown dashboard with a session TOC and recent activity (ctrl+shift+f10); PI_TERN_BRIDGE=0 disables |
| Tool results panel | experimental | a second Tern plugin rendering what your tools produced as native widgets: a git diff as a real diff view, a file read as highlighted code, a test run as a pass/fail/skip meter. Open with ctrl+shift+f11. Classified by lib/toolresults.ts; falls back to muted text when nothing fits, because a confident-looking 0/0/0 meter is worse than the text it replaced |
| Golden shots | stable | tern_shot renders scenario files to PNG + layout JSON (tern shot) |
| UI test harness | stable | tern_ui_test / /tern ui-test <scenario> [expect] runs a scenario and asserts against a control endpoint |
| Diagram pipelines | stable | --from "<cmd>" renders mermaid from a command's output; git renders the repo as a gitGraph |
| Credential guard | stable | tern_db refuses credential-looking tables and agent/models stores unless allowSecret:true |
| Remote hosts | stable | tern_remote lists or discovers Tern remote hosts |
| Diagnostics | stable | tern_diagnose and /tern diagnose: environment, probe, relay round trip, control endpoint, persisted state |
| Control endpoint | stable | `/tern control [window |
| Restore | stable | /tern restore reopens the session mirror and the last pinned diagram; state survives Tern/pi restarts |
| Live tab title | stable | π <model> · <context> · <dir>, refreshed after startup and on every turn |
| Attention bell | opt-in | PI_TERN_BELL=1 rings Tern's bell when pi finishes a turn |
| Native Tern surfaces | opt-in | Tern owning the transcript/composer/dock. Reached through the pi-tern launcher, not the extension: pi runs under a loader hook that hands rendering to Tern. Only in an agent block, and only inside Tern — see Limitations |
Requirements
- Tern 0.5.1 or newer, running (closed beta; a Stencil account comes from Tern itself). Verified against 0.5.2 (
21a6de4): the TSP vocabulary is byte-identical to 0.5.1 — same 44 node kinds in the same order, same 10 features, samehellofields — and the plugin window API is byte-identical (tern.d.luau, md5d39773c7…). 0.4.5 was the floor when the extension first shipped. - pi 1.0.x
- macOS or Linux. Not functional inside tmux, screen or zellij: those swallow APC strings, so the TSP handshake never completes and the pane-only features stay inert. The CLI-backed tools (
tern_chart,tern_worktree,tern_pr) do not depend on the handshake and keep working.
Do I need to run pi inside Tern?
No. Only the pane-scoped features do (TSP probe, live tab title, relay push, native
surfaces). Everything else needs just a running Tern daemon, so a pi-tern tool from a
T3-hosted agent, a cron job or a plain terminal can still open diagrams and charts in the
Tern window, drive Tern's browser, run and capture panes, spawn fleet panes, and manage
worktrees and PRs. That is what /tern capabilities reports per feature, with a reason when
a feature is unavailable here.
Native surfaces need an agent block, not a shell block
This is the one thing that will look broken if you get it wrong. Tern displays a TSP surface only in an agent block. Started in a shell block, pi-tern's frames are accepted and the surface materialises — but nothing is ever drawn, so the pane is blank, with no error anywhere to read.
agent block → pi's transcript, composer and status render as Tern surfaces ✔
shell block → blank pane, no error, no clue �’
So either:
Open an agent block deliberately — Tern's palette can create one beside the focused pane, or
Run
/tern agent-setupinside pi. It writes the two settings Tern documents for this, after backing up the file, and reports exactly what changed:Key Tern's description Value new_blocks"what new tabs and splits open" "Agent"agent_command"what a block runs: the login shell, or this" (defaults to omp)the pi-tern launcher Or set them by hand:
// ~/Library/Application Support/Tern/settings.json
{ "agent_command": "/Users/you/bin/pi-tern", "agent_placement": "split" }
And you can verify which kind you are in: /tern doctor reports the block kind, and pi-tern warns
when it detects a shell block.
Since 1.1.5 pi-tern detects this and recovers. At session start it asks the
pi-bridgeplugin what kind of block it is in; if it is not an agent block it hands the terminal back to pi's own interface and tells you why, instead of leaving a blank pane.PI_TERN_SKIP_KIND_CHECK=1disables the check. The kind is only exposed to a window plugin (cx.session:panes()), which is why the check goes through the mailbox —tern inspect --jsoncarries client kinds only, andtern whoamicarries just the identity chain.
Install
pi install git:github.com/Ghost-9/pi-tern
or
pi install npm:pi-tern
Manual install:
git clone https://github.com/Ghost-9/pi-tern ~/.pi/agent/extensions/pi-tern
Reload pi, then check inside a Tern pane:
pi
/reload
/tern doctor
Usage
Commands:
/tern doctor probe result, hello kinds, features, credits
/tern diagnose environment, relay and control diagnostics
/tern run [--last] <command> run a shell command in a visible Tern pane
/tern db <path> <sql> query SQLite through Tern's engine (read-only)
/tern doc <action> <path> read/outline/search/append/write a Tern document
/tern board read <path> read a native Tern board
/tern ask|remember|recall <text> talk to Carly (remote providers; no vault content)
/tern schedule <when> | <title> · tasks · cancel <id>
/tern settings get|list|describe <key>
/tern notebook read <pane> read an open notebook block
/tern notebook run <path> execute a notebook via nbconvert in a visible pane
/tern ui-test <scenario> [expect] shot + control-endpoint assertions
/tern diagram git [dir] render the repository history as a gitGraph
/tern diagram --from "<cmd>" render mermaid printed by a command
/tern mirror search <text> search the session mirror
/tern restore reopen the mirror and the last pinned diagram
/tern control [window|headless] start a control endpoint for tern_ctl
/tern bridge install|refresh|status install the pi-bridge Tern canvas
/tern diagram <mermaid> render a diagram through Tern's merman engine
/tern diagram --last [--pin] render the newest Mermaid fence in the conversation
/tern mirror on|off|open mirror the conversation into a Markdown block
/tern browser {"op":"open","url":"…"}
/tern capture [block] print a pane's visible text
/tern panes list sessions, tabs and blocks as JSON
/tern title | bell apply the title, or ring the bell once
Model-callable tools: tern_status, tern_run, tern_browser are declared; tern_diagram, tern_capture, tern_panes, tern_ctl, tern_mirror, tern_watch, tern_diagnose, tern_shot, tern_remote, tern_bridge, tern_db, tern_doc, tern_board, tern_carly, tern_notebook, tern_settings, tern_ui_test are deferred and callable from codemode scripts by name (listed in tern_status).
Examples:
Draw the architecture as a Mermaid diagram and open it in Tern.
Open https://example.com in Tern's browser, snapshot it, click the first link and capture a screenshot.
Start a session mirror so I can read this conversation natively.
Configuration
| Variable | Default | Effect |
|---|---|---|
PI_TERN_PROBE |
1 |
0 skips the TSP hello probe |
PI_TERN_TITLE |
1 |
0 stops title updates |
PI_TERN_BELL |
0 |
1 rings the bell at agent end |
PI_TERN_MIRROR |
0 |
1 starts the session mirror automatically |
PI_TERN_DIAGRAM_AUTO |
0 |
1 auto-opens Mermaid blocks found in replies |
PI_TERN_RELAY |
1 |
0 uses the tern browser CLI instead of the daemon relay |
PI_TERN_RUN |
1 |
0 disables the tern_run tool |
PI_TERN_BRIDGE |
1 |
0 skips the automatic pi-bridge link |
PI_TERN_FORCE |
— | 1 tries Tern features outside a Tern pane |
Prompt footprint
Only three tools are declared to the model — tern_status, tern_run, tern_browser. The other
sixteen are deferred: they do not appear in the prompt and are callable from codemode scripts by
name (the names are listed in tern_status). Measured cost of the whole extension: +774 prompt
tokens (21,704 → 22,478), down from +1,309 in 0.8.0 and +3,679 in 0.7.0. See
docs/BENCHMARKING.md.
How it works
sequenceDiagram
participant P as pi + pi-tern
participant T as Tern pane
P->>T: TSP hello query plus DA1
T-->>P: TSP hello reply (v1, 44 kinds, 10 features, credits 2)
T-->>P: DA1 device attributes
Note over P: probe confirmed, Tern detected
P->>T: tern open diagram.md (merman renders it)
P->>T: daemon relay: hello, welcome, browser op
T-->>P: relay answer ok
Three channels, kept separate:
- pty / TSP — the
helloprobe only. The extension does not open surfaces or send frames. - Tern daemon relay (
$TERN_PANE_SOCKET) — the browser. Frames areu32LE length + UTF-8 JSON; falls back to thetern browserCLI. ternCLI —openfor diagrams and the mirror,capture,ls --json,ctl.
docs/ARCHITECTURE.md has the module map and the probe lifecycle.
Limitations
- Native mode is opt-in and best-effort; the extension half is the supported half. pi-tui's ANSI
frames are multi-write synchronized-output transactions with private cursor accounting, so a second
writer tears them — which is why the extension never writes TSP frames. Native surfaces instead
run through the
pi-ternlauncher's loader hook, which depends on pi's internals and is rebased per pi release. The split is deliberate and documented: extension = stable and supported; native = opt-in. Outside Tern, in a shell block, or with-p/--mode json|rpc, native mode falls back to stock pi rather than failing. - Tern's agent layer is omp-keyed.
cx.agents:transcriptreads theomp.sessionsurface and returns{}for non-omp programs, so the Agent chip, Carly transcripts and prompt injection are unavailable. Tern 0.5.1 adds Hermes as a second native agent that speaks omp's chat vocabulary, which suggests this is an implementable contract rather than an omp-only privilege — but the read-side predicate for a third agent is unconfirmed, and impersonatingomp.sessionto reach it is deliberately not attempted. - Tern file blocks are not panes.
tern capturecannot read a file block back (no such pane in this session daemon), so diagram rendering is verified visually. - A session open across a Tern update keeps a stale TSP vocabulary. The daemon used to lag the
binary, which made this look like a platform problem; Tern 0.5.2 fixed that — "the session
daemon switches to the new build as the first window attaches, with your programs still running" —
and verified here: after one window attach a new pane's hello reported
ver: "0.5.2"while a long-running session carried on. So the remedy is attach any window, not restart the daemon. What remains is ours: the probe runs at session start, so a session that spans an update acts on the vocabulary it saw then./tern diagnosereports the version it probed with; re-probe after attaching a new window. - Native surfaces are agent-block-only (see above). In a shell block the pane is blank and nothing reports an error; 1.1.5 detects it and falls back, earlier versions did not.
- Browser capture needs a rendered picture-in-picture. Tern answers
capture: a 0×0 px image is out of rangewhile the PiP is not visible.tern_chartsidesteps this for SVG charts by rasterizing locally (rsvg-convert→inkscape→qlmanage→ browser); for HTML/JS pages the browser is still the only renderer. - Inline figures in the conversation are opt-in and unconfirmed.
PI_TERN_INLINE_IMAGES=1only attempts it; every rejection lands innativeState().lastError. The mechanism is settled: there is noblobframe op — Tern answersunknown op blobfor both["blob",id,mime,data]and["blob",mime,data], and the blob store is a plugin-VM API (blob(self, bytes, mime)), not part of the frame dialect. pi-tern therefore sends the bytes inline in theimagenode, which Tern accepts. The accepted encoding is not the same as seen rendering: use the file block orpng: truefor anything that must work today. Full table indocs/TSP-ENCODINGS.md. - The TSP handshake is a race and is retried. Three attempts with backoff, because on identical
panes one probe received Tern's 633-byte reply immediately while another received nothing across
six retries over 2.4 s. A single 700 ms attempt fails nondeterministically, which is why native
mode used to engage only sometimes.
/tern diagnosenow reportsprobeFailure—timeout(a lost race),no-hello-reply(the terminal answered the DA1 sentinel but does not speak TSP), orunexpected-reply:…— instead of failing silently. Tune withPI_TERN_PROBE_ATTEMPTSandPI_TERN_PROBE_TIMEOUT_MS. tern_ctlneeds a control endpoint. Run/tern control(headless by default) or launch Tern with--control EP/ setTERN_WINDOW_SOCKET.- The pi-bridge canvas is experimental. The plugin loads and binds its chord (verified in Tern's log and
plugin list), but the Luau canvas rendering itself has not been visually verified from CI. tern_watch --expectis event-driven. It subscribes once totern eventsrather than starting atern captureprocess every 500 ms, and falls back to backing-off polls if events do not flow. The reply reportsvia: "events"orvia: "poll", so the fallback is visible.- Mailbox latency is one plugin poll (~0.25–0.75 s per call); DB access is read-only unless
execis explicitly allowed;agent.db-style stores hold credentials, so pass explicit paths and never select secret columns. - npm 1.1.8 was a partial snapshot and is superseded. A release-workflow bug published it from a
mid-work commit, so the tarball is missing several of this release's files. Install 1.1.9 or
later. The bug is fixed: a tag whose version disagrees with
package.jsonnow fails the release. - Notebook execution is not exposed by Tern's plugin API;
tern_notebookonly reads open notebook blocks.
Security
pi extensions run inside the pi process with the same operating-system permissions. Read the source before installing, and only install from sources you trust.
- The session mirror is opt-in and writes to
~/.pi/agent/scratch/pi-tern/session-mirror.md. - The browser uses Tern's own WKWebView profile, including its cookies. Treat it as your logged-in browser context.
- The extension sends nothing anywhere; Tern is local.
Report vulnerabilities through the private advisory form.
What is verified, and how
This section exists because of a specific failure. Releases 1.1.0–1.1.4 recorded native surfaces as
"verified" on the strength of frame acceptance — Tern answered with zero errors and the surface
tree came back from capture --surfaces. That method cannot tell a surface that renders from one
that renders nothing, and both statements are equally true of a blank pane. The result was a P0
that shipped: native mode removed pi's entire interface and showed nothing at all.
So the distinction is stated rather than assumed:
| Claim | How it is checked |
|---|---|
| Frame accepted | Tern's own error channel. Strong, but it is acceptance — not display. |
| Layout rendered | Nothing has been verified this way, ever, and it is currently impossible: surfaces display only in an agent block, and on Tern 0.5.1 every harness command that creates one times out after 20 s — for any command. scripts/render-proof.mjs runs on every gate and reports the measured answer (today: BLOCKED); --require makes it a failure. Treat every native render claim as unverified until that probe reports a displayed surface. Full detail and reproduction: docs/RENDER-PROOF.md. |
| Works outside a Tern pane | scripts/verify.ts, 19 live checks run deliberately from outside a pane — the property that makes pi-tern usable from another host. |
| Never breaks stock pi | native/compat.mjs — 7 checks locally, --static (4, no model call) in CI. Differential: the launcher's stdio must be shape-identical to stock pi's, with no TSP frame in either. |
| Types are real | tsc --noEmit under strict, against the actual pi and typebox types. native/ is covered too, and scripts/check-native-types.mjs fails when a hand-written declaration names an export the module lacks. |
Run the whole thing with npm run gate. It prints a count per step and reports skips separately:
GATE PASSED (7 run, 0 skip) is not the same claim as GATE PASSED (5 run, 2 skip).
Development
npm install # dev dependencies, including the real pi + typebox types
npm test # the unit suite (96 tests), no Tern required
npm run test:live # relay/browser test; needs a Tern pane
npm run typecheck # strict, against the real pi types
npm run lint
npm run gate # everything, including the live checks (~30 s)
npm run bench # mailbox latency; needs a Tern pane with pi-bridge
The extension itself has no runtime dependencies: Node builtins, pi's APIs and TypeBox schemas only. pi and typebox are devDependencies, used to typecheck against the real API.
To regenerate the Release pages from a new tag, release.yml now runs the full suite, typecheck,
lint, the declaration check and the static compat tier before publishing to npm, then creates the
Release. Every tag has a Release page, and v1.1.2 carries a warning: it has a data-plane P0, fixed
in v1.1.3.
Credits
- The Tern Surface Protocol is documented by Stencil Labs: https://docs.stencil.so/tern/protocol/. Tern is a Stencil Labs product.
- pi is by Earendil Works.
- The browser relay framing and op vocabulary were informed by oh-my-pi (MIT), the reference TSP client.
- Not affiliated with Stencil Labs or Earendil Works.
License
MIT © 2026 Mayank Batra
Native mode (v1.0.0): run pi-tern instead of pi inside Tern for a native surface — transcript rows in main, a real TSP editor composer (Tern edit/undo/send drive pi), suspend/resume, opt-in surface adopt. Outside Tern or in -p/--mode json|rpc, it runs stock pi. See docs/NATIVE-FINDINGS.md.