pi-smart-terminal0.5.0
Persistent PTY terminal sessions for pi: overrides the built-in bash tool with a stateful shell, adds terminal_* tools, a live session overlay (/term) and footer status. Powered by smart-terminal-mcp.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-smart-terminal
A real, persistent shell for pi — the agent's bash tool runs in
a live PTY terminal session instead of a throwaway shell, powered by
smart-terminal-mcp's core.
Why
pi's built-in bash tool spawns a fresh shell for every command. Each call
runs in a vacuum:
bash: cd packages/api && npm test # 'cd' is lost instantly — so the agent
bash: cd packages/api && npm run build # must chain && cd ../.. into everything
bash: export API_KEY=... # environment gone by the next call
bash: npm run dev # background server: output unreachable,
# process dead or orphaned after the call
bash: python # REPLs, npm-init prompts, ssh, docker
# login — anything interactive just hangs
With pi-smart-terminal, bash runs in one persistent PTY session — same
shell, same terminal, alive for the whole conversation:
bash: cd packages/api # the session is now IN packages/api
bash: npm test # just works — no chaining
bash: export API_KEY=... # still set on the next call
bash: npm run dev # keeps running; new output readable at any time
# (terminal_read / terminal_wait / terminal_watch)
/term # YOU watch the agent's shell live, as it types
In short:
- State that sticks — cwd, environment variables and background processes
survive between calls. The agent stops wasting tokens on
cdchains and absolute paths. - Interactive programs work — REPLs, installers with prompts,
ssh, test-watchers: a real TTY means they behave like they do in your own terminal, and the agent can answer prompts withterminal_write/terminal_send_key. - Long-running processes become usable — start a dev server once, then read its output incrementally, wait for a "listening on" line, or watch for errors — event-driven, instead of re-dumping logs into the conversation.
- Codemode can drive it — every tool returns typed
structuredContent(with anoutputSchema), so pi'scodemodescripts and nestedctx.executeTool()callers orchestrate sessions programmatically (run → parse → retry loops) without parsing JSON text. - Session state survives compaction — the live session list (ids, cwd, busy) is injected into the model context on every request, so the model never forgets its sessionIds or their drifted cwd — even after compaction.
- You can actually watch it —
/termis a live viewer of the agent's shell. See what the model is doing in real time, scroll its full history, while the footer shows busy state and cwd at a glance (auto mode stays silent while idle). - Nothing leaks — all sessions are killed (entire process group) when pi exits; no orphaned dev servers after a session.
The terminal_* tool family adds the controls a persistent shell needs
(terminal_read, terminal_wait, terminal_watch, terminal_retry,
terminal_diff, …) — payload-identical to the smart-terminal-mcp tools, so
agent habits transfer 1:1 from Claude Code, Cursor & co.
How it integrates
- bash override — built-in
bashtransparently executes in the persistent PTY session (pi's rendering, truncation and timeouts preserved). Falls back to a one-shot shell when the session is busy with a background command. - terminal tools — 15
terminal_*tools matching the MCP server; extras load on demand viaterminal_tools, the built-intool_search, or adefaultToolspin. - compact tool rendering — every
terminal_*call renders as a one-line summary with a status badge (exit code, matched pattern, session count…); on pi ≥ 1.0.1 this usespi.registerToolRenderer(), so calls to deferred/unregistered tools — includingterminal_*calls in resumed sessions and HTML exports — render compactly too. /term— live session viewer (Tab / Shift+Tab switches sessions, structured header, three-zone status bar); footer — auto shows busy/exited states only.- Shared shell — opt-in: your
!commands run in the agent's session too. - Lifecycle — all PTYs killed (process group) on shutdown; if
node-ptyfails to load, pi starts normally without this extension.
Codemode, tool_search and error semantics
The tools are first-class citizens of pi's codemode ecosystem (pi ≥ 0.99):
- Namespace
smart-terminal— codemode scripts find the tools withsearchTools()and read usage guidance viadescribeNamespace("smart-terminal"). - Structured results — every tool declares an
outputSchemaand returnsstructuredContent; the model still receives the JSON text you know. - Failure with data —
terminal_run(failed success criteria),terminal_wait(pattern never appeared) andterminal_watch(timeout without a match) returnisError: trueresults: the model sees the failure, scripts still get the structured payload.terminal_execkeeps exit codes as data (MCP semantics). - Annotations — observers (
terminal_read/list/wait/watch/get_history/run_paged) carryreadOnlyHint,terminal_stop/terminal_write_filecarrydestructiveHint, so permission extensions can gate them accordingly. - Deferred extras — the 8 extra tools register with
exposure: "deferred": callable by codemode scripts and findable by the built-intool_searchat any time, but never auto-declared to the model. Activate them with theterminal_toolsloader, or pin e.g."defaultTools": ["+terminal_watch"](a pin survives; pi ≥ 0.99.2 re-enables it via/reload` too). - Compact rendering (pi ≥ 1.0.1) —
pi.registerToolRenderer()draws everyterminal_*call as a one-line summary plus a status badge: exit code and drifted cwd forterminal_exec/terminal_retry, match result forterminal_wait/terminal_watch, session count forterminal_list, colored+/−stats forterminal_diff. Because the resolver works by tool name rather than registration, it also renders calls to deferred tools that were never activated, andterminal_*calls in resumed sessions and HTML exports — even when the tool behind them is not registered (rendering is display-only and independent of the native PTY chain). On older pi the registration is skipped and pi's default rendering applies.
The session-state injection uses pi's context_with_system boundary: one
trailing system message per request carrying a terminal_sessions prompt
section (the same mechanism as pi's mcp_servers section). It is stateless —
never persisted to history — so the transcript stays clean and the provider
prefix remains cache-stable.
Install
pi install npm:pi-smart-terminal
Requirements: pi ≥ 0.99.2 (compact terminal_* rendering via registerToolRenderer on pi ≥ 1.0.1), Node ≥ 20 and a C++ toolchain for node-pty (prebuilt
binaries cover common platforms; on Windows use npm rebuild node-pty inside
the package if needed).
Configuration
~/.pi/agent/smart-terminal.json (all keys optional):
{
"overrideBash": true,
"userBash": false,
"bashTimeoutMs": 600000,
"footer": "auto",
"defaultShell": null,
"allToolsActive": false,
"sessionStateContext": true
}
| Key | Default | Meaning |
|---|---|---|
overrideBash |
true |
Replace built-in bash with the persistent-session backend. |
userBash |
false |
Route user ! commands through the shared session (opt-in; full-screen tools like !vim are better left native). |
bashTimeoutMs |
600000 |
Hard cap when the model passes no timeout. |
footer |
"auto" |
Footer status line: "auto" (only while a command runs or a session exited), "minimal" (always-on glyph + count, e.g. ⏵ 2), "full" (verbose id/name/cwd line), "off". true/false still work (= full/off). |
defaultShell |
null |
Force a shell; null = auto-detect (pwsh > powershell > cmd on Windows, $SHELL > bash > sh elsewhere). |
allToolsActive |
false |
Register all 15 tools with full schemas instead of lazy loading extras. |
sessionStateContext |
true |
Inject live session state (ids, cwd, busy) into the model context each request — survives compaction. |
How the bash override behaves
- First call spawns the agent session and waits for the shell banner (~1–2.5 s once).
- cwd persistence is the point:
cdcarries over between calls. The per-call cwd is only used when (re-)creating a dead session. Watch drift in the footer, or reset withterminal_stop+ a fresh bash call. - Parallel bash calls / busy session (a background command is running): transparently falls back to a one-shot stateless execution so the call still succeeds.
- Timeout (
timeout:N) and abort (Esc →ctrl+cto the PTY) map onto pi's native error formats; partial output is preserved. - Long-running commands (dev servers): use
terminal_execwithquietExitMs, or let a bash call time out — the command keeps running in the session; read new output withterminal_read({ since }), wait for patterns withterminal_wait/terminal_watch, and watch it live in/term.
Multi-line payloads (here-strings, heredocs)
The PTY feeds the shell line by line, so multi-line shell constructs pasted in one go get mangled:
- PowerShell here-strings (
@'...'@): PSReadLine can eat the@'opener mid-paste, and every apostrophe in the body then breaks parsing (Unexpected token ...). - POSIX heredocs (
<<EOF): the completion markersterminal_execappends land on the closing-delimiter line, so the heredoc never terminates and the session hangs until the timeout.
That's why terminal_exec (and the bash override, which uses the same
path) rejects them up front with the workaround instead of hanging the
session. Do this instead:
- Write the payload to a file: the
writetool, orterminal_write_file(resolves relative paths against the session's drifted cwd). - Reference the file:
gh pr create --body-file .pr-body.md,command < input.txt, or--body "$(Get-Content file -Raw)".
terminal_run is unaffected: it spawns a one-shot process with no PTY and no
markers, so heredocs genuinely work there. And if a command ever times out
with the shell stuck at a continuation prompt (>> / > / More?), the
result's hint names the prompt and how to recover: send the closing line
with terminal_write, or abort with terminal_send_key("ctrl+c").
Commands
/term— live overlay of terminal sessions (opens the most recent; Tab / Shift+Tab to switch, ↑↓/PgUp/PgDn scroll,ffollow,qclose)/terminals— list active sessions
Tool catalog
Active from the start: terminal_start, terminal_exec, terminal_run,
terminal_read, terminal_write, terminal_wait, terminal_stop,
terminal_list, plus the terminal_tools loader.
Deferred (load on demand): terminal_run_paged, terminal_retry,
terminal_diff, terminal_resize, terminal_send_key, terminal_get_history,
terminal_write_file, terminal_watch. Codemode scripts can call them as-is;
tool_search finds them; terminal_tools or a defaultTools pin activates
them for the model.
Payload shapes are identical to the smart-terminal-mcp tools, so agent habits transfer 1:1 between pi and MCP clients.
Relationship to smart-terminal-mcp
This package is the native pi integration of the same core (smart-terminal-mcp): one core, two adapters. The MCP server remains the portable path for Claude Code, Cursor and friends; inside pi, this extension goes deeper than MCP can — tool schemas, dynamic loading, custom UI, lifecycle hooks.
License
MIT