DocsInteractive terminal

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

TypeScriptPythonDefaultWhat it does
cols, rowssize: PtySizerequiredTerminal size in characters. PtySize takes rows then cols.
onDataon_data (async only)required in TypeScript and async PythonCalled with each chunk of output. The sync Python create() has no callback.
timeoutMstimeout60000 ms / 60 sHow long the SDK stays attached to the terminal.
useruserthe template's userLinux user the shell runs as. The base template's user is user.
envsenvs{} plus TERM, LANG and LC_ALLEnvironment variables for the shell.
cwdcwdthat user's home directoryWorking 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.