Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ flate2 = "1.1.9"
interprocess = "2.4.2"
portable-pty = "0.9.0"
regex = "1.12.4"
rquickjs = { version = "0.12.2", features = ["parallel"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.150"
sha2 = "0.10.9"
Expand Down
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ prints a session's effective timeouts.

| Command | Description |
| ------------------------------------------------------------ | ------------------------------------------- |
| `open [--shell S] [--cols N --rows N] [--cwd D] [--env K=V] [--timeout-<class> MS]` | Spawn a shell session. |
| `open [--shell S] [--backend B] [--cols N --rows N] [--cwd D] [--env K=V] [--timeout-<class> MS]` | Spawn a shell session. |
| `run <program> [args...]` | Spawn a session running a program directly. |
| `sessions` | List active sessions. |
| `close [--all]` | Close the current session (or all). |
Expand Down Expand Up @@ -306,12 +306,46 @@ With `--json`, failures also carry a `"kind"` field (`assertion`/`usage`/`no_ses
- nushell
- cmd

## Terminal backends

A session runs on one of two emulators, chosen at open time with `--backend`:

| Backend | Notes |
| ------------------ | ---------------------------------------------------------------------- |
| `alacritty` | Default. Native, no interpreter. |
| `xtermjs` | `@xterm/headless` on an embedded QuickJS. Matches what VS Code's terminal shows. |

```bash
shell-use open --backend xtermjs
shell-use run --backend xtermjs -- htop
```

Both backends pass the same conformance suite, so `expect`, `snapshot`, and the
SVG renderer behave identically on either. Pick `xtermjs` when the question is
specifically "does this look right in VS Code". Neither backend needs Node
installed: the xterm.js bundle is embedded in the binary.

Differences that are inherent to the emulators rather than to this wiring:

- Only `xtermjs` reports the `blink` attribute; alacritty parses SGR 5 and
discards it.
- Only `alacritty` keeps an underline color on a cell it is not underlining.
- **Narrowing a session reflows on `alacritty` and truncates on `xtermjs`.**
Resizing `abcdefghijklmnop` from 10 columns to 6 gives `abcdef/ghijkl/mnop`
on alacritty and `abcdef/klmnop` on xterm.js, which drops what no longer
fits. Avoid narrowing a session you still need the scrollback of.
- Reading the full scrollback (`--full`) is roughly 10x slower on `xtermjs`,
since every cell crosses a JS boundary. The visible screen is unaffected.

`--cols 0` is not a usable size on either emulator, so any request below 2x1 is
clamped to it and reported back at the clamped size.

## Comparison

| | shell-use | [tui-use](https://github.com/onesuper/tui-use) | [terminal-use](https://github.com/flipbit03/terminal-use) |
| ------------------------------------ | ------------------------------------------------ | ---------------------------------------------- | --------------------------------------------------------- |
| Language | Rust | TypeScript/Node | Rust |
| Emulator | alacritty | xterm (headless) | alacritty |
| Emulator | alacritty or xterm.js, per session | xterm (headless) | alacritty |
| Shell command tracking | ✅ command boundaries, exit codes, cwd | ❌ | ❌ |
| Testing / snapshots | ✅ `expect` text / output / exit-code / snapshot | ❌ | ❌ |
| Color & per-cell attributes | ✅ fg/bg, ANSI-256/hex/rgb, `cells` | ❌ plain text (+ highlights) | via PNG |
Expand Down
13 changes: 12 additions & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ without parsing text:

| Command | Description |
| --- | --- |
| `open [--shell S] [--cols N] [--rows N] [--cwd D] [--env K=V]...` | Spawn a shell session (auto-starts the daemon). `--env` is repeatable. |
| `open [--shell S] [--backend B] [--cols N] [--rows N] [--cwd D] [--env K=V]...` | Spawn a shell session (auto-starts the daemon). `--env` is repeatable. |
| `run <program> [args...] [--cols N] [--rows N] [--cwd D] [--env K=V]...` | Spawn a session running a program directly (no shell). |
| `sessions` | List active sessions. |
| `close [--all]` | Close the current session (or every session with `--all`). |
Expand Down Expand Up @@ -299,6 +299,17 @@ of `ShellUseError`. On its first call a client also checks that the daemon's
version matches the package and raises `VersionMismatchError` if they differ;
stop the daemon (`daemon_stop`) so it restarts on the matching binary.

## Terminal backends

`--backend B` picks the emulator a session runs on, on both `open` and `run`:
`alacritty` (default, native) or `xtermjs` (`@xterm/headless` on an embedded
QuickJS, matching VS Code's terminal). Node is not required for either.

Both pass the same conformance suite, so assertions and screenshots behave the
same on either. Two emulator-level differences remain: only `xtermjs` reports
`blink`, and only `alacritty` keeps an underline color on a cell it is not
underlining. `state` reports the backend in use.

## Supported shells & integration

`open --shell S` accepts: `bash`, `zsh`, `fish`, `powershell`, `pwsh`, `cmd`,
Expand Down
6 changes: 6 additions & 0 deletions bindings/js/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,9 @@ export class ShellUse {
cwd: opts.cwd ?? null,
env: envPairs(opts.env),
};
if (opts.backend !== undefined) {
payload.backend = opts.backend;
}
if (opts.waitReady !== undefined) {
payload.wait_ready = opts.waitReady;
}
Expand All @@ -316,6 +319,9 @@ export class ShellUse {
cwd: opts.cwd ?? null,
env: envPairs(opts.env),
};
if (opts.backend !== undefined) {
payload.backend = opts.backend;
}
if (opts.waitReady !== undefined) {
payload.wait_ready = opts.waitReady;
}
Expand Down
1 change: 1 addition & 0 deletions bindings/js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type { ErrorKind } from "./errors.js";
export { VERSION } from "./version.js";
export type {
ArtifactOptions,
Backend,
Cell,
ClientOptions,
Color,
Expand Down
23 changes: 19 additions & 4 deletions bindings/js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export type Shell =
| "elvish"
| "nushell";

/**
* The terminal emulator a session runs on. Both pass the same conformance
* suite, so this selects whose reading of an ambiguous sequence you see, not
* which features you get. Pick `"xtermjs"` to match what VS Code's terminal
* would show. Defaults to `"alacritty"`.
*/
export type Backend = "alacritty" | "xtermjs";

export interface Cursor {
x: number;
y: number;
Expand All @@ -43,21 +51,26 @@ export interface Cell {
inverse: boolean;
invisible: boolean;
strike: boolean;
/** Always `false` from the alacritty backend, which cannot report blink. */
/** Only the `xtermjs` backend reports blink; `alacritty` always says `false`. */
blink: boolean;
/** Shorthand for `underline_style !== "none"`. */
underline: boolean;
underline_style: UnderlineStyle;
/**
* `"default"` means the underline follows the text color. Tracked
* independently of `underline_style`, so a cell that set SGR 58 without an
* underline still reports the color it would use.
* `"default"` means the underline follows the text color.
*
* On the `alacritty` backend this is tracked independently of
* `underline_style`, so a cell that set SGR 58 without an underline still
* reports the color it would use. The `xtermjs` backend discards the color
* for a cell it is not drawing an underline on, and reports `"default"`.
*/
underline_color: Color;
}

export interface State {
session_shell: string | null;
/** The emulator this session is running on; absent from older daemons. */
backend?: Backend;
cols: number;
rows: number;
cursor: Cursor;
Expand Down Expand Up @@ -104,6 +117,8 @@ export interface SpawnOptions {
waitReady?: boolean;
retries?: number;
timeouts?: Timeouts;
/** Emulator to run the session on; defaults to `"alacritty"`. */
backend?: Backend;
}

export interface Timeouts {
Expand Down
6 changes: 6 additions & 0 deletions bindings/python/src/shell_use/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ async def open(
self,
*,
shell: Optional[str] = None,
backend: Optional[str] = None,
cols: int = cfg.DEFAULT_COLS,
rows: int = cfg.DEFAULT_ROWS,
cwd: Optional[str] = None,
Expand All @@ -268,6 +269,8 @@ async def open(
"cwd": cwd,
"env": env_pairs(env),
} # type: Dict[str, Any]
if backend is not None:
payload["backend"] = backend
if wait_ready is not None:
payload["wait_ready"] = wait_ready
session_timeouts = cfg.session_timeouts_payload(timeouts)
Expand All @@ -279,6 +282,7 @@ async def run(
self,
program: str,
*args: str,
backend: Optional[str] = None,
cols: int = cfg.DEFAULT_COLS,
rows: int = cfg.DEFAULT_ROWS,
cwd: Optional[str] = None,
Expand All @@ -296,6 +300,8 @@ async def run(
"cwd": cwd,
"env": env_pairs(env),
} # type: Dict[str, Any]
if backend is not None:
payload["backend"] = backend
if wait_ready is not None:
payload["wait_ready"] = wait_ready
session_timeouts = cfg.session_timeouts_payload(timeouts)
Expand Down
3 changes: 3 additions & 0 deletions bindings/python/src/shell_use/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class State:
ready: bool
text: str
session_shell: Optional[str]
backend: str

@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "State":
Expand All @@ -71,4 +72,6 @@ def from_dict(cls, d: Dict[str, Any]) -> "State":
ready=d.get("ready", False),
text=d.get("text", ""),
session_shell=d.get("session_shell"),
# Absent when talking to a daemon that predates backend selection.
backend=d.get("backend", "alacritty"),
)
Loading