Interactive terminal
A terminal is an interactive login shell inside a sandbox that you drive with raw bytes instead of one command at a time.
Use it when the session has to keep state between keystrokes: a REPL, a program that asks questions, or a terminal you render in a browser. For one-off commands use Run commands instead.
Every example reads IMPELLO_API_KEY from the environment. Make a key at
dashboard.impello.ai/keys.
Create a terminal
sandbox.pty.create() starts /bin/bash -i -l attached to a pseudo-terminal
of the size you give it and returns a handle. The handle's pid identifies the
terminal in every later call.
import { Sandbox } from '@impello/sdk'
const sandbox = await Sandbox.create('base')
const decoder = new TextDecoder()
const terminal = await sandbox.pty.create({
cols: 80,
rows: 24,
onData: (data) => {
process.stdout.write(decoder.decode(data, { stream: true }))
},
})
console.log(terminal.pid)Output arrives as bytes, not text. In TypeScript onData receives a
Uint8Array for every chunk the shell writes; decode it with a streaming
TextDecoder so a multi-byte character split across two chunks survives. The
sync Python create() takes no callback — read the output by iterating the
handle, or pass on_pty to wait(), both shown below. Python hands you the
raw bytes either way, so decode them with an incremental decoder for the same
reason.
The shell starts with TERM=xterm-256color, LANG=C.UTF-8 and
LC_ALL=C.UTF-8. Anything you pass in envs overrides those.
Options
| TypeScript | Python | Default | What it does |
|---|---|---|---|
cols, rows | size: PtySize | required | Terminal size in characters. PtySize takes rows then cols. |
onData | on_data (async only) | required in TypeScript and async Python | Called with each chunk of output. The sync Python create() has no callback. |
timeoutMs | timeout | 60000 ms / 60 s | How long the SDK stays attached to the terminal. |
user | user | the template's user | Linux user the shell runs as. The base template's user is user. |
envs | envs | {} plus TERM, LANG and LC_ALL | Environment variables for the shell. |
cwd | cwd | that user's home directory | Working directory the shell starts in. |
Raise timeoutMs (timeout in Python, in seconds) to stay attached longer. In
Python, timeout=0 removes the limit entirely. TypeScript has no such value —
a timeoutMs of 0 is a deadline that has already passed, so the call fails
at once; pass a large number instead.
The thing that ends the session is the sandbox's own timeout: when it runs out
the terminal goes with everything else inside it — killed, or paused if the
sandbox's lifecycle says so. See Timeouts.
Send input
Send keystrokes as bytes with pty.sendInput() in TypeScript or
pty.send_stdin() in Python — a name difference beyond the casing. Include the
newline; the shell does not run a line until it sees one.
const encoder = new TextEncoder()
await sandbox.pty.sendInput(terminal.pid, encoder.encode('echo hello\n'))
await sandbox.pty.sendInput(terminal.pid, encoder.encode('exit\n'))
await terminal.wait()
await sandbox.kill()Control characters are bytes like any other: send \x03 for Ctrl-C and \x04
for Ctrl-D. wait() returns once the shell exits, and a non-zero exit code
raises CommandExitError (CommandExitException in Python); see
Errors.
The handle is a CommandHandle, the same class a background command returns,
but a terminal has no separate stdin. handle.sendStdin() and
handle.closeStdin() (handle.send_stdin() and handle.close_stdin() in
Python) throw SandboxError (SandboxException in Python) on
it. Always send input through sandbox.pty.
Resize
Call resize() whenever the window the terminal is drawn in changes size, so
full-screen programs redraw at the right width.
await sandbox.pty.resize(terminal.pid, { cols: 120, rows: 40 })Reconnect
A terminal keeps running after the handle that created it goes away, so you can
pick it up from another process or after a network drop. connect() takes the
pid and returns a fresh handle.
sandbox.commands.list() returns every running command, terminal and template
start command as a ProcessInfo. A terminal is the entry whose args start
with -i: commands run as /bin/bash -l -c <command>, terminals as
/bin/bash -i -l.
import { Sandbox } from '@impello/sdk'
const sandbox = await Sandbox.connect(process.env.SANDBOX_ID as string)
const decoder = new TextDecoder()
const running = await sandbox.commands.list()
const shell = running.find((p) => p.args[0] === '-i')
if (shell) {
const terminal = await sandbox.pty.connect(shell.pid, {
onData: (data) => {
process.stdout.write(decoder.decode(data, { stream: true }))
},
timeoutMs: 60 * 60 * 1000,
})
await terminal.wait()
}The sync Python handle is iterable and yields (stdout, stderr, pty) tuples
with one field set; for a terminal it is always the third.
connect() takes the same timeoutMs (timeout) as create(), with the same
60 second default. The examples above pass their own bound so they stay
attached for longer. Call handle.disconnect() to stop receiving output
without killing the shell.
Kill
pty.kill() sends SIGKILL to the shell. It returns true when the terminal
was killed and false when no process has that pid.
await sandbox.pty.kill(terminal.pid)handle.kill() does the same for the handle's own pid. Killing the sandbox
kills every terminal in it.
Async Python
AsyncSandbox has the same pty module with every method awaited. The async
create() and connect() take an on_data callback, because there is no sync
iterator to read from, and wait() takes no callbacks.
import asyncio
import codecs
from impello import AsyncSandbox, PtySize
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
async def main():
sandbox = await AsyncSandbox.create("base")
terminal = await sandbox.pty.create(
PtySize(rows=24, cols=80),
on_data=lambda data: print(decoder.decode(data), end=""),
)
await sandbox.pty.send_stdin(terminal.pid, b"echo hello\n")
await sandbox.pty.send_stdin(terminal.pid, b"exit\n")
await terminal.wait()
await sandbox.kill()
asyncio.run(main())
Next: Filesystem.