Tabby demo plugin prototype¶
Question and scope¶
Can the experimental receiver run through Tabby’s normal plugin seams: a
TerminalDecorator for the tab and xterm instance, and a SessionMiddleware
for bidirectional session bytes? This prototype uses the official
TerminalDecorator,
SessionMiddleware,
and XTermFrontend
interfaces at tabby-terminal@1.0.231-nightly.0.
The receiver composes the existing terminal Endpoint, mixed-stream ingress,
and experimental region-aware xterm history. Ordinary output uses Tabby’s
BaseTerminalTabComponent.write() and stays ordered with protocol rendering;
a parser-completion barrier then lets the region tracker sample the realized
cursor. A persistent streaming UTF-8 decoder preserves characters split across
session chunks. Protocol responses return through outputToSession. The
middleware is inserted before Tabby’s OSC processor. Input uses the existing
middleware path unchanged.
See the stock and protocol side-by-side walkthrough; its verification recording stays local, and a presentation recording is pending.
This is a development spike, not a stable plugin release. It still depends on
private xterm.js internals through PrivateCoreBlockHistory. The long-session
policy below reclaims closed Context snapshots and fully evicted render
projections. Abandoned open Contexts, slow consumers, steady-state behavior
beyond the measured session, and platforms other than Windows bundled ConPTY
remain open. Independent protocol applications can run sequentially in one tab:
each SDK client has a distinct request-ID namespace, so control replay cannot
return an earlier application’s closed Context. Multiple rounds within one Pi
process are supported.
Plugin-only xterm compatibility¶
The plugin deliberately retains ordinary output and protocol Blocks in one xterm history; a second xterm instance would no longer demonstrate that shared history. Using private internals is an acceptable limitation for this research plugin, not a requirement for terminal vendors: a native integration controls its own buffer implementation and does not need this adapter.
Before activating a terminal session, compatibility.ts reads Tabby’s own
tabby-terminal/package.json declaration and checks the private surface used
by the history adapter: _core._bufferService, the active buffer’s numeric
state, lines.get/splice, markers, blank-line copying, and _onScroll.fire.
Only the tested ^5 declaration is accepted. Tabby 1.0.238’s manifest
uses that declaration, and its official lockfile
resolves xterm.js 5.5.0. Tabby bundles xterm.js without exposing its package
through require("@xterm/xterm/package.json"), so the runtime check combines
Tabby’s declared version range with the actual private structure; it is not an
independent read of the exact bundled xterm.js version.
A version mismatch, missing or malformed field, or receiver construction failure prevents receiver construction and middleware insertion and leaves passthrough unchanged. The capability query therefore receives no response, allowing the application to take its normal stock-rendering path. One shared decorator gate reports the compatibility failure once per loaded plugin instance, rather than once per tab. New or forked hosts must be verified before their version declaration is accepted.
Build and check¶
From the repository root:
pnpm install
pnpm build:tabby-demo
node --test prototypes/integration/tabby-demo/*.test.ts
The build writes a self-contained UMD plugin to
.tmp/tabby-demo-plugin/, with package.json, README.md, and LICENSE.
Tabby, Angular, and RxJS remain external; protocol, terminal, and host code are
bundled. Workspace-only dependency declarations are removed from the prepared
package.
The tests cover split UTF-8 and SDK negotiation, response routing, ordinary output, historical Block Update, lifecycle, compatibility rejection before receiver construction, and one-time notification. Reclamation checks cover 20 sequential Contexts without retained Session snapshots, full-eviction projection cleanup, and native reset after reclamation. Real SDK children run through Windows bundled ConPTY, including two sequential children on one receiver. The desktop evidence below complements those checks.
Desktop evidence, 2026-10-26¶
The official Tabby 1.0.238 portable package, verified against its published SHA-256, loads the built plugin through the normal
TABBY_PLUGINSpath.Ordinary CMD input/output works. The SDK child completes with
SDK_EXIT:0, ordinary markers before and after the protocol, and a historical Block Update.Two independent SDK children run sequentially in one Tabby tab. Both exit 0, receive distinct
context-1andcontext-2, and close cleanly. Both updated Block histories remain present; neither child receives the other’s cached closed Context.The real Pi stock UI completes two consecutive rounds with the deterministic local provider. Both Contexts and all eight Blocks are closed/sealed; a 700/1002-pixel viewport round trip preserves them. Repeating the two-round check with the compatibility guard produced two closed Contexts and eight sealed Blocks, including both distinct user prompts.
An artifact-only fault injection changed the accepted declaration to
^999while Tabby supplied^5. Two terminal tabs had no protocol middleware and retained passthrough. The first attachment logged one compatibility warning; the second produced no further warning. Real Pi printed its unsupported- protocol message, entered stock mode, and completed one round with one copy of the prompt and result. The injected change was confined to.tmp.The initial second-round failure isolated a timing requirement: Tabby’s small-write flow-control path may resolve before xterm has parsed the data. Waiting for a subsequent xterm write callback before sampling the cursor removed that failure in this check. This is evidence for the tested path, not a claim that every terminal frontend or long session is covered.
Long-session and reclamation evidence, 2026-10-27¶
The retention policy releases closed Session snapshots after rendering drains, without reclaiming open or invalidated Contexts or changing control replay and idempotent closure. The renderer keeps visible Blocks for native reading and releases fully evicted projection content. Its invalidation path uses renderer-owned ranges, so reclaimed Session snapshots are not needed for later cleanup. Receiver metrics are cumulative counters, not retained snapshots.
Before this policy, the same real-Tabby check completed 16 rounds and 34 Blocks,
then failed the seventeenth context.open with Local session budget exhausted: Context records. With the policy enabled, one Pi process in one Tabby tab
completed all 20 rounds. All 20 Contexts and 42 Blocks closed/sealed; no Session
Context snapshots remained after completed rounds, 42 Blocks remained rendered,
all 20 user prompts occurred exactly once, and no protocol errors or invalidated
Contexts were observed. A 700x900/1002x753 viewport round trip then preserved
those counts and the first and last Block ranges.
Two sequential SDK children in another tab also exited 0 with reclamation
enabled. The counters showed distinct context-1 and context-2, each with one
sealed Block and no retained Session snapshot; both updated Blocks remained
rendered and neither child received the other’s cached closed Context.
The memory samples below used CDP Runtime.getHeapUsage after an explicit GC,
process.memoryUsage() for renderer RSS, and PowerShell Get-Process for the
browser and renderer working sets (WS). The driver checked both PID roles rather
than relying on process order. Values are from one run, not a steady-state bound.
Round |
Closed Contexts |
Closed Blocks |
Session Contexts |
JS heap (MiB) |
Browser WS (MiB) |
Renderer WS (MiB) |
|---|---|---|---|---|---|---|
0 |
0 |
0 |
0 |
49.24 |
188.18 |
224.06 |
5 |
5 |
12 |
0 |
50.31 |
188.61 |
225.27 |
10 |
10 |
22 |
0 |
50.50 |
190.21 |
225.56 |
15 |
15 |
32 |
0 |
50.59 |
191.68 |
225.96 |
20 |
20 |
42 |
0 |
50.68 |
190.76 |
225.69 |
JS heap increased by 1,513,508 bytes (1.44 MiB); renderer RSS increased from 222.85 to 225.76 MiB. Retained terminal history and Pi’s own conversation can still grow. Control replay remains capped at 1,024 results, and xterm capacity policy still applies. This demonstrates bounded closed-snapshot reclamation and measured behavior for the demo, not unlimited connection lifetime or flat process memory. Behavior beyond 20 rounds, abandoned Contexts, and slow consumers remains unverified.
With the built plugin loaded in a visible Tabby started with
--remote-debugging-port=9222, reproduce the 20-round check from the repository
root:
node prototypes/integration/tabby-demo/long-session-check.ts --rounds 20 --pace 5 --output .tmp/tabby-long-after-20.json
The driver opens a new CMD tab, uses the deterministic local provider, checks
all rounds and retained prompts, and writes the complete report. The validation
above used separate Tabby configuration and Chromium userData directories.
Its CDP/Angular inspection is experimental test tooling, not an additional
production integration API.
Try in Tabby¶
Build the plugin, then copy
.tmp/tabby-demo-plugininto the directory opened by Tabby’s Settings → Plugins → Open Plugins Directory. Restart Tabby.Open one ordinary shell tab and run
echo ordinary shell; input and output should remain ordinary terminal behavior.Change to this repository checkout in a Tabby tab and run:
node prototypes/integration/pi-stock-ui/main.tsThis starts the real Pi stock UI with its deterministic local provider, not a model subscription. With the plugin working, the transcript is protocol-owned while Pi keeps its editor and footer. Without a supporting terminal, the same child uses Pi’s stock rendering fallback. Submit further prompts in that Pi process to exercise multiple rounds. After it exits, another independent protocol application can run in the same tab.
Do not claim that upstream Pi natively emits the protocol: the trial composition enables it without editing Pi’s source files.