Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bae84a9
Wait for touch events in direct_touch tests instead of sleeping
mgasner Sep 28, 2026
89158cc
Keep the exit record of a process that exits before WAIT
mgasner Sep 28, 2026
0a48a38
Speak the YAS byte stream on the Unix socket transports
mgasner Sep 28, 2026
dbd0862
Emit Node-loadable ESM from core, react and solid
mgasner Sep 28, 2026
394b643
Call the wasm init export through the loaded module
mgasner Sep 28, 2026
54aab99
Track the release in core's @yas-run/browser peer range
mgasner Sep 28, 2026
7171605
Document direct as the touchMode default in React and Solid views
mgasner Sep 28, 2026
7870c88
Resolve target names from the home server's remotes catalogue
mgasner Sep 28, 2026
cd80c67
Stop creating yas.remotes when reading it
mgasner Sep 28, 2026
7d427d9
Return empty output for a sequence cursor past the end
mgasner Sep 28, 2026
a76205d
Document OUTPUT behavior for a cursor past the end
mgasner Sep 28, 2026
8522323
Size terminal OUTPUT collection limits for the result framing
mgasner Sep 28, 2026
a7f5a6d
Make `yas terminal output --wait` time out with 124
mgasner Sep 28, 2026
687e438
Tolerate a closed stdout in terminal commands
mgasner Sep 28, 2026
f77d828
Refuse `yas terminal send` to an exited or unknown terminal
mgasner Sep 28, 2026
c1b9050
Fix terminal restart, server env flags, and missing-program status
mgasner Sep 28, 2026
e9997c7
Print output of extension attempts that finish before ext run follows…
mgasner Sep 28, 2026
a527d51
Document ext run output, trap, and name-retention behavior
mgasner Sep 28, 2026
efdf9a0
Describe ext manage --from as a registry URL
mgasner Sep 28, 2026
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
17 changes: 11 additions & 6 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ server's own KV store — see below.
attached backend workspace; they are never transferred by the edge.

Special key: `yas.target = <uri-or-name>` — sets the default for non-browser
CLI commands. Browsers discover Relay routes from the home server and keep the
CLI commands. A name is resolved against the home server's catalogue (below)
on each connection. Browsers discover Relay routes from the home server and keep the
active route set in the attached backend workspace.

### The Relay catalogue — the `remotes` KV key
Expand All @@ -252,11 +253,15 @@ two catalogues rather than one shared home-directory file; and KV already has
watching, compare-and-swap and a client-facing family, so editing a remote
needed no transport of its own.

Managed with `yas remote add/remove/toggle/list`, which now reach the target
server rather than this machine's home directory — `yas --on dev remote add`
edits `dev`'s catalogue — and from the browser's Remotes panel. A server that
Managed with `yas remote add/remove/toggle/list`, which now reach a server
rather than this machine's home directory: the home server by default (not
`yas.target`), or the `--on` server, so `yas --on dev remote add` edits `dev`'s
catalogue. Also managed from the browser's Remotes panel. A server that
finds no `remotes` key at startup imports a pre-KV `yas.remotes` file once, if
there is one.
there is one. The CLI resolves a bare target name (`--on NAME`, `YAS_TARGET`,
`yas.target`) by reading this key from the home server (`YAS_SOCK`, else the
default local instance) and dialing the stored URI itself; it no longer reads
the file.

**The stored URIs carry credentials, and every client of the server can read
them**: a `share:` passphrase, an `ssh:` host reference. That is the trade for
Expand Down Expand Up @@ -318,7 +323,7 @@ All yas components share a common URI vocabulary for addressing yas server insta
| `share:passphrase` | CLI, `yas.remotes` | Native read-only WebRTC share |
| `share:passphrase?hub=URL` | `yas.remotes` | WebRTC via custom hub URL |
| `proxy:uri` | CLI (`--on`) | Explicitly route through yas proxy-daemon |
| `name` | CLI (`--on`), yas.conf | Named remote from yas.remotes |
| `name` | CLI (`--on`), yas.conf | Named remote on the home server |

Set `YAS_PROXY=0` to bypass proxy routing and connect directly for `ssh:`,
`tcp:`, `ws:`, `wss:`, and `wt:` URIs.
Expand Down
27 changes: 22 additions & 5 deletions EMBEDDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,12 @@ You can also run a `@yas-run/core` client **server-side** (Node/Bun/Deno) to dri
local `yas server` over its unix-domain socket — e.g. to script terminals or run
headless commands. The non-browser building blocks live under the
`@yas-run/core/node` subpath (kept out of the package root so `node:net` and
runtime globals never leak into browser bundles):
runtime globals never leak into browser bundles). The packages are plain ES
modules; install `@yas-run/core` with its matching `@yas-run/browser` peer:

```bash
npm install @yas-run/core @yas-run/browser
```

```ts
import { YasWorkspace, exitCodeFromStatus, nullLogger } from "@yas-run/core";
Expand All @@ -315,6 +320,17 @@ const workspace = new YasWorkspace({
connections: [{ id: "default", transport }],
});

// Families are negotiated after the HELLO round trip; wait before creating.
await new Promise<void>((resolve) => {
const check = () => {
if (!workspace.getSnapshot().ready) return;
unsubscribe();
resolve();
};
const unsubscribe = workspace.subscribe(check);
check();
});

const session = await workspace.createSession({
connectionId: "default",
rows: 24,
Expand All @@ -323,10 +339,11 @@ const session = await workspace.createSession({
});
```

The unix transport speaks yas's framing protocol (4-byte little-endian
length-prefixed frames) for you — there is no need to re-implement the wire
format. `BunUnixSocketTransport` and `DenoUnixSocketTransport` are the
runtime-native equivalents.
The unix transport carries the YAS byte stream (the preface, then 4-byte
little-endian length-prefixed frames) for you; there is no need to
re-implement the wire format. `BunUnixSocketTransport` and
`DenoUnixSocketTransport` are the runtime-native equivalents; Deno needs
`--allow-read --allow-write` for the socket path.

### Exit status

Expand Down
2 changes: 1 addition & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ description: >
yas is a terminal multiplexer and experimental headless Wayland compositor. Every terminal can run both CLI programs (via PTYs) and GUI applications (via the built-in compositor). Surfaces are video-encoded and streamed to browsers; the CLI gives programmatic control over both terminals and graphical windows.

Everything the CLI does works locally or against a remote over one wire —
`--on ssh:host`, `--on share:passphrase`, or a named remote — including the
`--on ssh:host`, `--on share:passphrase`, or a named remote from `yas remote` — including the
filesystem, git, and language-server commands. Beyond terminals it can:

- **Files** — `yas fs cat|find|grep|write|sync|mkdir|mv|rm|ln`. `grep`
Expand Down
9 changes: 9 additions & 0 deletions bin/prepare-release
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,13 @@ node - "$target_version" "${js_manifests[@]}" <<'NODE'
const fs = require("fs");
const [version, ...manifests] = process.argv.slice(2);

// @yas-run/browser is built from crates/browser outside the pnpm workspace,
// so `workspace:*` cannot stamp its published range; it tracks the release.
for (const manifest of manifests) {
const pkg = JSON.parse(fs.readFileSync(manifest, "utf8"));
pkg.version = version;
if (pkg.peerDependencies?.["@yas-run/browser"] !== undefined)
pkg.peerDependencies["@yas-run/browser"] = `^${version}`;
fs.writeFileSync(manifest, `${JSON.stringify(pkg, null, 2)}\n`);
}
NODE
Expand Down Expand Up @@ -341,6 +345,11 @@ for (const manifest of manifests) {
console.error(`release: failed to update ${manifest} to version ${expected}`);
process.exit(1);
}
const browser = pkg.peerDependencies?.["@yas-run/browser"];
if (browser !== undefined && browser !== `^${expected}`) {
console.error(`release: failed to update ${manifest} @yas-run/browser peer to ^${expected}`);
process.exit(1);
}
}
NODE

Expand Down
7 changes: 7 additions & 0 deletions crates/alacritty-driver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,13 @@ impl TerminalDriver {
out.evicted = true;
}
out.start_seq = seq;
// A cursor past the end of the output reads nothing and resumes from
// the end, so a result's start never lies after its next cursor.
if (seq, out.start_col) > (out.next_seq, out.next_col) {
out.start_seq = out.next_seq;
out.start_col = out.next_col;
return out;
}
if seq > end_inclusive {
return out;
}
Expand Down
23 changes: 23 additions & 0 deletions crates/alacritty-driver/tests/seq_cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,26 @@ fn growing_with_no_history_does_not_invent_rotation() {
assert_eq!(named_lines(&d, 3), before);
assert_eq!(d.cursor_seq(), cursor);
}

#[test]
fn a_cursor_past_the_end_reads_nothing_and_resumes_at_the_end() {
let mut d = TerminalDriver::new(10, 40, 1000);
feed_lines(&mut d, 0..3);
d.process(b"prompt> ");
let (seq, col) = d.cursor_seq();

for (from_seq, from_col) in [(seq, col + 5), (seq + 2, 0), (seq + 1000, 7)] {
let read = d.seq_text(from_seq, from_col, None, 64 * 1024);
assert_eq!(read.text, "");
assert!(!read.truncated);
assert_eq!((read.start_seq, read.start_col), (seq, col));
assert_eq!((read.next_seq, read.next_col), (seq, col));
}

let bounded = d.seq_text(seq + 50, 0, Some(seq), 64 * 1024);
assert_eq!(bounded.text, "");
assert_eq!(
(bounded.start_seq, bounded.start_col),
(bounded.next_seq, bounded.next_col)
);
}
41 changes: 26 additions & 15 deletions crates/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ pub struct Cli {
#[derive(Args, Clone)]
pub struct ConnectOpts {
/// Remote to connect to: a URI (ssh:host, tcp:h:p, socket:/p, share:pass, local[:name])
/// or a named remote from yas.remotes. Overrides YAS_TARGET and yas.conf `target`.
/// or the name of a remote on the home server (see `yas remote`). Overrides
/// YAS_TARGET and yas.conf `yas.target`.
#[arg(long, global = true)]
pub on: Option<String>,

Expand Down Expand Up @@ -397,11 +398,16 @@ pub enum Command {
/// Execute a process and connect its standard streams
Run(RunArgs),

/// Manage named remotes in yas.remotes
/// Manage the server's named remotes
///
/// Named remotes let you refer to frequently-used destinations by a short
/// name instead of a full URI. They are stored in ~/.config/yas/yas.remotes
/// (mode 0o600) and can also be set as the default target via `yas.conf`.
/// name instead of a full URI. They are stored in a server's KV store (the
/// `remotes` key), which is also the catalogue its Relay publishes to the
/// browser. These verbs edit the home server (YAS_SOCK, else the default
/// local instance) unless --on names another; YAS_TARGET and `yas.target`
/// do not redirect them. `--on NAME`, YAS_TARGET, and `yas.target` look
/// names up on the home server. A legacy ~/.config/yas/yas.remotes file is
/// imported once into a server that has no catalogue yet.
///
/// Examples:
/// yas remote add rabbit ssh:rabbit
Expand All @@ -421,9 +427,9 @@ pub enum Command {
#[command(
about = "Open the terminal UI in the browser",
long_about = "Open the terminal UI in the browser\n\n\
Opens the browser with all named remotes from ~/.config/yas/yas.remotes\n\
plus the local yas server. Manage remotes with `yas remote add/remove`\n\
or through the Remotes dialog in the browser.\n\n\
Opens the browser on the local yas server, with that server's named\n\
remotes reachable through its Relay. Manage remotes with\n\
`yas remote add/remove` or through the Remotes dialog in the browser.\n\n\
Examples:\n\
yas open # local + all configured remotes\n\
yas remote add rabbit ssh:rabbit\n\
Expand Down Expand Up @@ -594,7 +600,8 @@ pub enum Command {
fd_channel: Option<i32>,

/// Export the server socket path as YAS_SOCK in spawned terminals
/// (or set YAS_EXPORT_SOCK=1)
/// (or set YAS_EXPORT_SOCK=1). Without it, terminals still never
/// inherit a YAS_SOCK that names a different server
#[arg(long)]
export_sock: bool,

Expand Down Expand Up @@ -1019,9 +1026,11 @@ pub enum TerminalCommand {

/// Print one command's output (needs OSC 133 shell integration).
///
/// Defaults to the newest command. With --wait, blocks server-side until
/// the command finishes and exits with its status (124 if the wait timed
/// out), which is how to run something in a live shell and collect the
/// Defaults to the newest command. With --wait and no INDEX, it picks the
/// command that is running, or if none is, the next one to start (so a
/// command sent just before still counts), blocks server-side until that
/// command finishes, and exits with its status (124 if the wait timed
/// out). This is how to run something in a live shell and collect the
/// result:
/// yas terminal send 3 'cargo test\n'
/// yas terminal output 3 --wait 600
Expand Down Expand Up @@ -1277,6 +1286,7 @@ pub enum TerminalCommand {

/// Send input to a terminal.
///
/// Fails if the terminal has exited.
/// Supports C-style escapes: \n \r \t \\ \0 \xHH.
/// \n sends CR (Enter), matching real terminal behavior. Use \x0a for literal LF.
/// To control interactive programs like vim:
Expand Down Expand Up @@ -2660,17 +2670,18 @@ pub enum RemoteCommand {
},

/// Disable or enable a named remote without removing it.
/// Disabled remotes are kept in yas.remotes (commented out) and excluded
/// from connection resolution until re-enabled.
/// Disabled remotes stay in the catalogue but are not published by the
/// Relay, and `--on NAME` refuses them until re-enabled.
Toggle {
/// Name of the remote to toggle
name: String,
},

/// Set the default remote in yas.conf
///
/// After this, all agent subcommands (list, start, show, …) will connect
/// to this remote by default, without needing --on.
/// Writes `yas.target` in this machine's yas.conf. After this, commands
/// without --on connect to this remote. A name is looked up on the home
/// server at each connection.
SetDefault {
/// Name or URI to use as the default target.
/// Pass an empty string or "local" to reset to local.
Expand Down
20 changes: 20 additions & 0 deletions crates/cli/src/learn.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ yas terminal send "$ID" "\x03" # Ctrl+C
```

Supports C-style escapes: `\n`, `\t`, `\r`, `\\`, `\0`, `\xHH`. Use `-` to read from stdin.
Sending to an exited terminal fails with a nonzero exit instead of dropping the
input.

`\n` sends CR (0x0D), which is what a real terminal sends for Enter. This works
regardless of whether the program is in canonical or raw mode. `\r` also sends
Expand Down Expand Up @@ -142,6 +144,8 @@ yas terminal journal "$ID" --json
```

`--wait` blocks server-side until the command finishes (exit 124 on timeout).
Without an index it waits for the running command, or the next one to start if
none is running, so a finished command is never mistaken for the one just sent.
`wait --pattern` matches only output produced after the wait began.

## Terminal lifecycle
Expand Down Expand Up @@ -226,6 +230,14 @@ yas remote add prod ssh:alice@prod.co
yas remote set-default prod
```

A bare name (from `--on`, `YAS_TARGET`, or `yas.target` in `yas.conf`) is
looked up in the home server's remotes catalogue, the one `yas remote add`
edits: the server at `YAS_SOCK`, else the default local instance, started if
needed. A name may point at another name. Disabled remotes
(`yas remote toggle`) and unknown names fail with an error. `yas remote`
verbs edit the home server too unless `--on` names another server; the
default target does not redirect them.

## Files

All paths are relative to `--root` (default: the client's cwd, resolved
Expand Down Expand Up @@ -366,6 +378,14 @@ so `run`/`update` options go before the name. `--restart`, `--persist`,
`--detach` and `--json` written after the module are refused rather than
handed over; put a `--` first if the extension really wants one of them.

An attached `run` prints the attempt's stdout, stderr, and log output, even
when it finishes before the CLI starts following, and exits with the
extension's return code. A trapped or failed attempt prints
`yas: extension NAME trapped: DETAIL` to stderr and exits 1. A detached
transient extension keeps its name for 30 s after it stops
(`YAS_EXT_TERMINAL_RETAIN`) so its output can be replayed; `run` refuses to
reuse that name until then and says so.

`yas ext manage` opens an inline extension picker. Move with arrows or `j`/`k`
and use Space or a mouse click to cycle the selected action: install for new
extensions, update then uninstall for outdated ones, or uninstall for other
Expand Down
6 changes: 6 additions & 0 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1226,7 +1226,13 @@ fn mask_remote_credentials(uri: &str) -> String {
/// catalogue is (`crates/cli/src/yas_remotes.rs`). `set-default` stays local:
/// which server *this* CLI talks to by default is this machine's business, not
/// something a server should hold an opinion about.
///
/// Without `--on` the verbs edit the home server, not `YAS_TARGET` or
/// `yas.target`: names resolve against the home catalogue, so after
/// `set-default work` a `toggle work` must still reach the catalogue that
/// defines `work`.
async fn cmd_remote(cmd: RemoteCommand, on: Option<&str>, hub: &str) -> Result<(), String> {
let on = Some(on.unwrap_or("local"));
match cmd {
RemoteCommand::List { reveal } => {
let entries = yas_remotes::read(on, hub).await?;
Expand Down
Loading
Loading