From bae84a986353476fa071942ab24d7bb302127260 Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 20:02:32 +0000 Subject: [PATCH 01/19] Wait for touch events in direct_touch tests instead of sleeping Two tests timed out on loaded CI runners. The fixture requested wl_touch 50 ms after enabling touch, and a wl_touch created before the seat advertises the touch capability never receives events. direct_touch_has_its_own_browser_clock read events 80 ms after sending them. The fixture now waits for the touch capability, that test waits for its five motions, and dispatch waits up to 10 s. --- crates/compositor/tests/direct_touch.rs | 39 ++++++++++++++++++++----- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/crates/compositor/tests/direct_touch.rs b/crates/compositor/tests/direct_touch.rs index bee09217c..e348d5fa2 100644 --- a/crates/compositor/tests/direct_touch.rs +++ b/crates/compositor/tests/direct_touch.rs @@ -18,7 +18,7 @@ use wayland_client::protocol::{ wl_compositor, wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_registry, wl_seat, wl_surface, wl_touch, }; -use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle, delegate_noop}; +use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle, WEnum, delegate_noop}; use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; use yas_compositor::{ @@ -26,6 +26,9 @@ use yas_compositor::{ spawn_compositor_without_renderer, }; +/// Generous because CI runners are loaded; a passing run returns long before it. +const DISPATCH_TIMEOUT: Duration = Duration::from_secs(10); + /// A `wl_touch` event, reduced to what these tests pin. #[derive(Debug, PartialEq, Clone)] enum T { @@ -49,6 +52,7 @@ struct App { /// `time` of each motion, which is what an app differentiates to get a fling /// velocity. motion_times: Vec, + touch_capable: bool, } impl Dispatch for App { @@ -153,7 +157,23 @@ impl Dispatch for App { delegate_noop!(App: ignore wl_compositor::WlCompositor); delegate_noop!(App: ignore wl_surface::WlSurface); -delegate_noop!(App: ignore wl_seat::WlSeat); +impl Dispatch for App { + fn event( + state: &mut Self, + _: &wl_seat::WlSeat, + event: wl_seat::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let wl_seat::Event::Capabilities { + capabilities: WEnum::Value(capabilities), + } = event + { + state.touch_capable = capabilities.contains(wl_seat::Capability::Touch); + } + } +} delegate_noop!(App: ignore xdg_toplevel::XdgToplevel); delegate_noop!(App: ignore wl_data_device_manager::WlDataDeviceManager); delegate_noop!(App: ignore wl_data_device::WlDataDevice); @@ -220,8 +240,14 @@ impl Fixture { .send(CompositorCommand::SetTouchEnabled { enabled: true }) .expect("enable touch"); handle.wake(); - std::thread::sleep(Duration::from_millis(50)); - queue.roundtrip(&mut app).expect("capabilities roundtrip"); + // A wl_touch requested before the seat advertises touch never receives + // events, so wait for the capability rather than a fixed delay. + let deadline = Instant::now() + DISPATCH_TIMEOUT; + while !app.touch_capable { + assert!(Instant::now() < deadline, "seat never advertised touch"); + std::thread::sleep(Duration::from_millis(10)); + queue.roundtrip(&mut app).expect("capabilities roundtrip"); + } let touch = seat.get_touch(&qh, ()); queue.roundtrip(&mut app).expect("get_touch roundtrip"); @@ -281,7 +307,7 @@ impl Fixture { } fn dispatch_until(&mut self, done: impl Fn(&App) -> bool) { - let deadline = Instant::now() + Duration::from_secs(2); + let deadline = Instant::now() + DISPATCH_TIMEOUT; while !done(&self.app) { let remaining = deadline.saturating_duration_since(Instant::now()); assert!(!remaining.is_zero(), "timed out waiting for touch events"); @@ -584,8 +610,7 @@ fn direct_touch_has_its_own_browser_clock() { } handle.wake(); - std::thread::sleep(Duration::from_millis(80)); - f.queue.roundtrip(&mut f.app).expect("roundtrip"); + f.dispatch_until(|app| app.motion_times.len() >= 5); let deltas: Vec = f .app .motion_times From 89158cc652433398813e2ad1da207e92c7357df5 Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 20:34:06 +0000 Subject: [PATCH 02/19] Keep the exit record of a process that exits before WAIT A child that exited before SPAWN stored its handle left no WAIT replay, and the Exit handler removed the route before recording the replay. A WAIT in either window found neither and answered NotFound, which is how uplink_cli_remote_execution_and_authentication failed in CI on `yas run sh -c 'exit 7'`. The handle store and the exit record now serialize on the replay table, the route retires after its replay is recorded, and WAIT checks the replay again when the route is gone. --- crates/server/src/yas_process.rs | 81 +++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 12 deletions(-) diff --git a/crates/server/src/yas_process.rs b/crates/server/src/yas_process.rs index 05b55eab3..c0993e3b8 100644 --- a/crates/server/src/yas_process.rs +++ b/crates/server/src/yas_process.rs @@ -410,9 +410,17 @@ impl Session { self.remove_route(process_id); return Err(Error::Closed("mismatched Process SPAWN reply".to_owned())); } - route - .process_handle - .store(started.process_handle, Ordering::Release); + { + // Serialized with the Exit handler through `exits`: a child that + // exits before its handle is stored still leaves a WAIT replay. + let mut exits = self.inner.exits.lock().unwrap(); + route + .process_handle + .store(started.process_handle, Ordering::Release); + if let Some(exit) = route.exit.borrow().clone() { + exits.insert(started.process_handle, exit); + } + } Ok(Attachment { session: self.clone(), route, @@ -494,6 +502,16 @@ impl Session { let (mut exit, temporary) = if let Some(route) = self.route_by_handle(request.process_handle) { (route.exit.subscribe(), None) + } else if let Some(exit) = self + .inner + .exits + .lock() + .unwrap() + .get(request.process_handle) + .cloned() + { + // The route retires only after its replay is recorded. + return Ok(exit); } else { match self .watch_process(request.process_handle, false, true) @@ -874,18 +892,19 @@ fn dispatch_outbound(inner: &Arc, event: process::NativeEvent) -> .routes .lock() .unwrap() - .remove(&process_id) + .get(&process_id) + .cloned() .ok_or_else(|| Error::Closed("exit for unknown Process binding".to_owned()))?; let exit = native_exit_info(exit); - let process_handle = route.process_handle.load(Ordering::Acquire); - if process_handle != 0 { - inner - .exits - .lock() - .unwrap() - .insert(process_handle, exit.clone()); + { + let mut exits = inner.exits.lock().unwrap(); + route.exit.send_replace(Some(exit.clone())); + let process_handle = route.process_handle.load(Ordering::Acquire); + if process_handle != 0 { + exits.insert(process_handle, exit.clone()); + } } - route.exit.send_replace(Some(exit.clone())); + inner.routes.lock().unwrap().remove(&process_id); let _ = route.events.try_send(Event::Exit(exit)); Ok(()) } @@ -1182,6 +1201,44 @@ mod tests { server.shutdown().await; } + #[tokio::test] + async fn wait_after_a_fast_exit_returns_the_exit_record() { + let server = Server::new(false, true); + let runtime = Runtime::new(server.clone()); + let session = runtime.session([4; 16], None).unwrap(); + for _ in 0..50 { + let mut attachment = session + .spawn( + &spawn_request( + vec![executable("sh"), b"-c".to_vec(), b"exit 7".to_vec()], + Vec::new(), + ), + None, + ) + .await + .unwrap(); + let handle = attachment.process_handle; + while !matches!( + tokio::time::timeout(Duration::from_secs(5), attachment.next()) + .await + .unwrap() + .unwrap(), + Event::Exit(_) + ) {} + let exit = session + .wait(&wire::Wait { + process_handle: handle, + timeout_ns: 5_000_000_000, + extensions: Extensions::default(), + }) + .await + .unwrap(); + assert_eq!(exit.code, 7); + } + session.shutdown().await; + server.shutdown().await; + } + #[tokio::test] async fn empty_environment_is_exact_and_wait_observes_exit() { let server = Server::new(false, true); From 0a48a3850bfff16ac5bce6f4be13de5c1d3dd36e Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 18:02:52 +0000 Subject: [PATCH 03/19] Speak the YAS byte stream on the Unix socket transports AbstractUnixSocketTransport length-prefixed every send, including the 8-byte preface, and left yasFraming unset, so YasConnection used message framing and the server closed the link on the bad preface. The Node, Bun and Deno Unix socket transports never reached HELLO. The socket is a byte stream: the raw preface followed by u32 length-prefixed frames. Declare yasFraming = "stream" and pass bytes through unchanged in both directions, as the WebTransport, WebRTC and Relay transports do; YasConnection owns the framing. Bun's socket.write accepts only what fits in the kernel buffer and drops the rest, which corrupts a byte stream on large writes. Queue the remainder and flush it on drain. --- EMBEDDING.md | 9 ++- js/core/src/__tests__/unix-transport.test.ts | 79 +++++++++----------- js/core/src/transports/unix-base.ts | 44 ++--------- js/core/src/transports/unix-bun.ts | 28 ++++++- 4 files changed, 75 insertions(+), 85 deletions(-) diff --git a/EMBEDDING.md b/EMBEDDING.md index 34f3add6d..b8149f7d5 100644 --- a/EMBEDDING.md +++ b/EMBEDDING.md @@ -323,10 +323,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 diff --git a/js/core/src/__tests__/unix-transport.test.ts b/js/core/src/__tests__/unix-transport.test.ts index c9c5a930b..3cbb7a0ec 100644 --- a/js/core/src/__tests__/unix-transport.test.ts +++ b/js/core/src/__tests__/unix-transport.test.ts @@ -8,6 +8,8 @@ import { BunUnixSocketTransport } from "../transports/unix-bun"; import { NodeUnixSocketTransport } from "../transports/unix"; import type { AbstractUnixSocketTransport } from "../transports/unix-base"; import type { UnixSocketTransportOptions } from "../transports/unix-base"; +import { YasConnection } from "../yas/session"; +import { YAS_PREFACE } from "../yas/wire"; // These tests exercise real AF_UNIX sockets on POSIX. The transport is // intentionally local-only, so we skip on platforms where @@ -45,24 +47,17 @@ for (const { name, skip, make } of suites) { let sockPath: string; let server: Server; let lastClient: Socket | null = null; - const clientMessages: Buffer[] = []; + let clientBytes = Buffer.alloc(0); beforeEach(async () => { tmp = mkdtempSync(join(tmpdir(), "yas-unix-transport-")); sockPath = join(tmp, "yas.sock"); - clientMessages.length = 0; + clientBytes = Buffer.alloc(0); lastClient = null; server = createServer((socket) => { lastClient = socket; - let buf = Buffer.alloc(0); socket.on("data", (chunk: Buffer) => { - buf = buf.byteLength === 0 ? chunk : Buffer.concat([buf, chunk]); - while (buf.byteLength >= 4) { - const len = buf.readUInt32LE(0); - if (buf.byteLength < 4 + len) break; - clientMessages.push(Buffer.from(buf.subarray(4, 4 + len))); - buf = buf.subarray(4 + len); - } + clientBytes = Buffer.concat([clientBytes, chunk]); }); }); await new Promise((resolve) => server.listen(sockPath, resolve)); @@ -73,13 +68,6 @@ for (const { name, skip, make } of suites) { rmSync(tmp, { recursive: true, force: true }); }); - function framed(payload: Uint8Array): Buffer { - const out = Buffer.alloc(4 + payload.byteLength); - out.writeUInt32LE(payload.byteLength, 0); - out.set(payload, 4); - return out; - } - async function waitFor( probe: () => T | null | undefined, timeoutMs = 1000, @@ -110,54 +98,59 @@ for (const { name, skip, make } of suites) { t.close(); }); - it("sends length-prefixed frames", async () => { + it("is a YAS byte-stream transport", () => { const t = make(sockPath); - t.connect(); - await waitFor(() => t.status === "connected"); - t.send(new Uint8Array([1, 2, 3])); - await waitFor(() => clientMessages.length > 0); - expect(Array.from(clientMessages[0]!)).toEqual([1, 2, 3]); + expect(t.yasFraming).toBe("stream"); t.close(); }); - it("decodes length-prefixed frames from the server", async () => { + it("writes bytes unchanged", async () => { const t = make(sockPath); - const received: ArrayBuffer[] = []; - t.addEventListener("message", (d) => received.push(d)); t.connect(); await waitFor(() => t.status === "connected"); - await waitFor(() => lastClient !== null); - lastClient!.write(framed(new Uint8Array([0xaa, 0xbb]))); - lastClient!.write(framed(new Uint8Array([0xcc]))); - await waitFor(() => received.length === 2); - expect(Array.from(new Uint8Array(received[0]!))).toEqual([0xaa, 0xbb]); - expect(Array.from(new Uint8Array(received[1]!))).toEqual([0xcc]); + t.send(new Uint8Array([1, 2, 3])); + t.send(new Uint8Array([4])); + await waitFor(() => clientBytes.byteLength === 4); + expect(Array.from(clientBytes)).toEqual([1, 2, 3, 4]); t.close(); }); - it("handles split reads across frame boundaries", async () => { + it("delivers server bytes unchanged across chunk boundaries", async () => { const t = make(sockPath); - const received: ArrayBuffer[] = []; - t.addEventListener("message", (d) => received.push(d)); + const received: number[] = []; + t.addEventListener("message", (d) => received.push(...new Uint8Array(d))); t.connect(); await waitFor(() => t.status === "connected"); await waitFor(() => lastClient !== null); - const whole = framed(new Uint8Array([1, 2, 3, 4, 5])); - // Deliver one byte at a time — the framer must wait for the full frame. - for (let i = 0; i < whole.byteLength; i++) { - lastClient!.write(whole.subarray(i, i + 1)); - } - await waitFor(() => received.length === 1); - expect(Array.from(new Uint8Array(received[0]!))).toEqual([1, 2, 3, 4, 5]); + const whole = [0xaa, 0xbb, 0xcc, 0xdd, 0xee]; + for (const byte of whole) lastClient!.write(Uint8Array.of(byte)); + await waitFor(() => received.length === whole.length); + expect(received).toEqual(whole); t.close(); }); + it("starts a YAS session with the raw preface and a framed HELLO", async () => { + const t = make(sockPath, { reconnect: false }); + const connection = new YasConnection(t); + const hello = connection.connect(); + hello.catch(() => {}); + await waitFor(() => clientBytes.byteLength > YAS_PREFACE.length + 4); + expect(Array.from(clientBytes.subarray(0, YAS_PREFACE.length))).toEqual( + Array.from(YAS_PREFACE), + ); + const frameLength = clientBytes.readUInt32LE(YAS_PREFACE.length); + await waitFor( + () => clientBytes.byteLength === YAS_PREFACE.length + 4 + frameLength, + ); + connection.close(); + }); + it("send() is a no-op before connected", async () => { const t = make(sockPath); // Intentionally do not call connect(). t.send(new Uint8Array([1])); await new Promise((r) => setTimeout(r, 20)); - expect(clientMessages).toHaveLength(0); + expect(clientBytes.byteLength).toBe(0); t.close(); }); diff --git a/js/core/src/transports/unix-base.ts b/js/core/src/transports/unix-base.ts index dbfe61300..d7996caa5 100644 --- a/js/core/src/transports/unix-base.ts +++ b/js/core/src/transports/unix-base.ts @@ -7,9 +7,10 @@ import type { export interface UnixSocketTransportOptions extends YasTransportOptions {} /** - * Shared implementation of the yas local-IPC framing protocol - * (little-endian `u32` length prefix, see `crates/server/src/lib.rs` - * `read_frame`/`write_frame`). + * Shared implementation of the YAS local-IPC byte stream. The socket + * carries the raw preface followed by `u32` little-endian length-prefixed + * frames (see `docs/design/yas.md`, "Transport framing"); the transport + * passes bytes through unchanged and {@link YasConnection} does the framing. * * Concrete subclasses plug in a socket backend (Node's `net` module, * Bun's `Bun.connect`, ...) via {@link openRawSocket}. They receive @@ -17,6 +18,7 @@ export interface UnixSocketTransportOptions extends YasTransportOptions {} * via {@link onRawConnect}, {@link onRawClose} and {@link onRawError}. */ export abstract class AbstractUnixSocketTransport implements YasTransport { + readonly yasFraming = "stream" as const; readonly maxDatagramSize = 0; private _status: ConnectionStatus = "disconnected"; private reconnectTimer: ReturnType | null = null; @@ -29,8 +31,6 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { authRejected = false; lastError: string | null = null; - /** Incoming byte accumulator. */ - private recvBuf = new Uint8Array(0); /** Sentinel that tracks which socket attempt events belong to. */ protected currentAttempt: symbol | null = null; @@ -57,13 +57,6 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { send(data: Uint8Array): void { if (this._status !== "connected") return; - const header = new Uint8Array(4); - const len = data.byteLength; - header[0] = len & 0xff; - header[1] = (len >> 8) & 0xff; - header[2] = (len >> 16) & 0xff; - header[3] = (len >>> 24) & 0xff; - this.writeRaw(header); this.writeRaw(data); } @@ -73,7 +66,6 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { this.clearConnectTimer(); this.currentAttempt = null; this.destroyRawSocket(); - this.recvBuf = new Uint8Array(0); this.setStatus("closed"); } @@ -83,7 +75,6 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { this.clearConnectTimer(); this.currentAttempt = null; this.destroyRawSocket(); - this.recvBuf = new Uint8Array(0); this.currentDelay = this.initialDelay; this.setStatus("disconnected"); } @@ -94,7 +85,6 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { this.clearConnectTimer(); this.currentAttempt = null; this.destroyRawSocket(); - this.recvBuf = new Uint8Array(0); this.currentDelay = this.initialDelay; this.setStatus("disconnected"); this.connect(); @@ -159,7 +149,6 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { return; } this.setStatus("connecting"); - this.recvBuf = new Uint8Array(0); const attempt = Symbol("unix-attempt"); this.currentAttempt = attempt; @@ -211,25 +200,9 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { protected ingestChunk(attempt: symbol, chunk: Uint8Array): void { if (this.currentAttempt !== attempt || this.disposed) return; if (chunk.byteLength === 0) return; - if (this.recvBuf.byteLength === 0) { - const copy = new Uint8Array(chunk.byteLength); - copy.set(chunk); - this.recvBuf = copy; - } else { - const merged = new Uint8Array(this.recvBuf.byteLength + chunk.byteLength); - merged.set(this.recvBuf, 0); - merged.set(chunk, this.recvBuf.byteLength); - this.recvBuf = merged; - } - while (this.recvBuf.byteLength >= 4) { - const b = this.recvBuf; - const len = b[0]! | (b[1]! << 8) | (b[2]! << 16) | (b[3]! * 0x01000000); - if (this.recvBuf.byteLength < 4 + len) break; - const ab = new ArrayBuffer(len); - new Uint8Array(ab).set(this.recvBuf.subarray(4, 4 + len)); - this.recvBuf = this.recvBuf.subarray(4 + len); - for (const l of this.messageListeners) l(ab); - } + const ab = new ArrayBuffer(chunk.byteLength); + new Uint8Array(ab).set(chunk); + for (const l of this.messageListeners) l(ab); } protected onRawError(attempt: symbol, message: string): void { @@ -242,7 +215,6 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { if (this.currentAttempt !== attempt || this.disposed) return; this.clearConnectTimer(); this.currentAttempt = null; - this.recvBuf = new Uint8Array(0); this.setStatus("disconnected"); this.scheduleReconnect(); } diff --git a/js/core/src/transports/unix-bun.ts b/js/core/src/transports/unix-bun.ts index 385b769a3..a207e3bd8 100644 --- a/js/core/src/transports/unix-bun.ts +++ b/js/core/src/transports/unix-bun.ts @@ -18,6 +18,8 @@ type BunSocket = import("bun").Socket<{ attempt: symbol }>; */ export class BunUnixSocketTransport extends AbstractUnixSocketTransport { private socket: BunSocket | null = null; + /** Bytes Bun's non-buffering `write` did not accept yet, flushed on drain. */ + private pending: Uint8Array[] = []; constructor(path: string, options?: UnixSocketTransportOptions) { super(path, options); @@ -40,9 +42,15 @@ export class BunUnixSocketTransport extends AbstractUnixSocketTransport { this.onRawConnect(attempt); }, data: (_sock, chunk: Uint8Array) => this.ingestChunk(attempt, chunk), + drain: (sock) => { + if (this.socket === sock) this.flush(); + }, error: (_sock, err: Error) => this.onRawError(attempt, err.message), close: (sock) => { - if (this.socket === sock) this.socket = null; + if (this.socket === sock) { + this.socket = null; + this.pending = []; + } this.onRawClose(attempt); }, }, @@ -54,10 +62,26 @@ export class BunUnixSocketTransport extends AbstractUnixSocketTransport { } protected writeRaw(data: Uint8Array): void { - this.socket?.write(data); + if (!this.socket) return; + this.pending.push(data); + if (this.pending.length === 1) this.flush(); + } + + private flush(): void { + const socket = this.socket; + while (socket && this.pending.length > 0) { + const head = this.pending[0]!; + const written = socket.write(head); + if (written < head.byteLength) { + this.pending[0] = head.subarray(Math.max(written, 0)); + return; + } + this.pending.shift(); + } } protected destroyRawSocket(): void { + this.pending = []; const socket = this.socket; if (!socket) return; this.socket = null; From dbd08625d924dd9ed7324cc7ac9616a5e2252c07 Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 18:03:19 +0000 Subject: [PATCH 04/19] Emit Node-loadable ESM from core, react and solid tsc with moduleResolution "bundler" emitted relative imports exactly as written, without extensions, so importing the published @yas-run/core or @yas-run/react dist from Node ESM failed with ERR_MODULE_NOT_FOUND. Write explicit .js (or /index.js) extensions on every relative import and compile with module/moduleResolution NodeNext, so tsc rejects an extensionless relative import instead of shipping it. Solid's Vite build already rewrote its JS; its emitted .d.ts now resolve under NodeNext too. --- js/core/src/AudioPlayer.ts | 2 +- js/core/src/SurfaceStore.ts | 10 +- js/core/src/TerminalStore.ts | 8 +- js/core/src/YasNativeWorkspaceConnection.ts | 72 +++++----- js/core/src/YasSurfaceCanvas.ts | 39 +++--- js/core/src/YasTerminalSurface.ts | 34 +++-- js/core/src/YasWorkspace.ts | 32 ++--- js/core/src/desktopModel.ts | 2 +- js/core/src/downscale.ts | 2 +- js/core/src/fsModel.ts | 2 +- js/core/src/gitModel.ts | 2 +- js/core/src/gl-renderer.ts | 2 +- js/core/src/index.ts | 146 +++++++++++--------- js/core/src/keyboard.ts | 4 +- js/core/src/layout/index.ts | 8 +- js/core/src/layout/tree.ts | 6 +- js/core/src/lspModel.ts | 2 +- js/core/src/mediaModel.ts | 8 +- js/core/src/node-wasm.ts | 2 +- js/core/src/node.ts | 10 +- js/core/src/palettes.ts | 2 +- js/core/src/surfaceColor.ts | 8 +- js/core/src/transports/browser.ts | 20 +-- js/core/src/transports/index.ts | 26 ++-- js/core/src/transports/noise.ts | 6 +- js/core/src/transports/unix-base.ts | 2 +- js/core/src/transports/unix-bun.ts | 4 +- js/core/src/transports/unix-deno.ts | 4 +- js/core/src/transports/unix.ts | 4 +- js/core/src/transports/uplink.ts | 6 +- js/core/src/transports/webrtc-share.ts | 4 +- js/core/src/transports/webrtc.ts | 2 +- js/core/src/transports/webtransport.ts | 2 +- js/core/src/webgpu-renderer.ts | 4 +- js/core/src/workspaceConnectionTypes.ts | 2 +- js/core/src/workspaceSessionDevices.ts | 8 +- js/core/src/workspaceSessions.ts | 14 +- js/core/src/yas/channel.ts | 12 +- js/core/src/yas/client.ts | 10 +- js/core/src/yas/core.ts | 6 +- js/core/src/yas/datagram.ts | 10 +- js/core/src/yas/defaults.ts | 4 +- js/core/src/yas/desktop.ts | 12 +- js/core/src/yas/edge.ts | 6 +- js/core/src/yas/env.ts | 10 +- js/core/src/yas/events.ts | 10 +- js/core/src/yas/extension.ts | 12 +- js/core/src/yas/font.ts | 14 +- js/core/src/yas/fs.ts | 12 +- js/core/src/yas/git.ts | 14 +- js/core/src/yas/index.ts | 72 +++++----- js/core/src/yas/kv.ts | 10 +- js/core/src/yas/lsp.ts | 14 +- js/core/src/yas/media.ts | 12 +- js/core/src/yas/nativeChannelFacade.ts | 12 +- js/core/src/yas/nativeDesktopMedia.ts | 25 ++-- js/core/src/yas/nativeExtensionFacade.ts | 8 +- js/core/src/yas/nativeProductFamilies.ts | 26 ++-- js/core/src/yas/nativeRelayTransport.ts | 6 +- js/core/src/yas/nativeWorkspaceFs.ts | 16 +-- js/core/src/yas/nativeWorkspaceGit.ts | 18 +-- js/core/src/yas/nativeWorkspaceKv.ts | 12 +- js/core/src/yas/nativeWorkspaceLsp.ts | 16 +-- js/core/src/yas/net.ts | 10 +- js/core/src/yas/packed.ts | 8 +- js/core/src/yas/process.ts | 12 +- js/core/src/yas/recording.ts | 6 +- js/core/src/yas/relay.ts | 16 +-- js/core/src/yas/selection.ts | 12 +- js/core/src/yas/session.ts | 12 +- js/core/src/yas/state.ts | 8 +- js/core/src/yas/surface.ts | 14 +- js/core/src/yas/terminal-grid.ts | 8 +- js/core/src/yas/terminal.ts | 16 +-- js/core/src/yas/terminalRenderer.ts | 6 +- js/core/src/yas/transfer.ts | 12 +- js/core/src/yas/wire.ts | 4 +- js/core/tsconfig.json | 4 +- js/react/src/YasSurfaceView.tsx | 2 +- js/react/src/YasTerminal.tsx | 8 +- js/react/src/hooks/index.ts | 10 +- js/react/src/hooks/useYasConnection.ts | 2 +- js/react/src/hooks/useYasSession.ts | 2 +- js/react/src/hooks/useYasSessions.ts | 2 +- js/react/src/hooks/useYasWorkspace.ts | 2 +- js/react/src/index.ts | 27 ++-- js/react/tsconfig.json | 4 +- js/solid/src/YasSurfaceView.tsx | 2 +- js/solid/src/YasTerminal.tsx | 4 +- js/solid/src/hooks/createYasSessions.ts | 2 +- js/solid/src/hooks/createYasWorkspace.ts | 2 +- js/solid/src/hooks/index.ts | 10 +- js/solid/src/index.ts | 22 +-- js/solid/tsconfig.json | 4 +- 94 files changed, 585 insertions(+), 558 deletions(-) diff --git a/js/core/src/AudioPlayer.ts b/js/core/src/AudioPlayer.ts index a7cd24c01..88d17a541 100644 --- a/js/core/src/AudioPlayer.ts +++ b/js/core/src/AudioPlayer.ts @@ -21,7 +21,7 @@ * from a Blob URL — no external file needed. */ -import { claimPlaybackAudioSession } from "./audioSession"; +import { claimPlaybackAudioSession } from "./audioSession.js"; /** * Maximum pre-worklet staging depth in decoded frames (~20 ms each). diff --git a/js/core/src/SurfaceStore.ts b/js/core/src/SurfaceStore.ts index b8dd9cb8d..82dff74ab 100644 --- a/js/core/src/SurfaceStore.ts +++ b/js/core/src/SurfaceStore.ts @@ -4,9 +4,9 @@ import { videoColorSpace, SDR_COLOR, rejectSurfaceColorProfile, -} from "./surfaceColor"; -import type { YasSurfaceColorSpace } from "./yas/packed"; -import type { YasSurface, ConnectionId, SurfaceId } from "./types"; +} from "./surfaceColor.js"; +import type { YasSurfaceColorSpace } from "./yas/packed.js"; +import type { YasSurface, ConnectionId, SurfaceId } from "./types.js"; import { SURFACE_FRAME_FLAG_KEYFRAME, SURFACE_FRAME_CODEC_MASK, @@ -15,11 +15,11 @@ import { CODEC_SUPPORT_AV1, CODEC_SUPPORT_H264_444, CODEC_SUPPORT_AV1_444, -} from "./surfaceModel"; +} from "./surfaceModel.js"; // Shared with the codec probe rather than duplicated: the probe answers // for what this browser accepts at a given level, and a decoder configured // here at a different one would be asking a question nobody answered. -import { av1LevelString, av1SequenceProfile } from "./videoCodec"; +import { av1LevelString, av1SequenceProfile } from "./videoCodec.js"; /** Configure from the encoded stream, not the pane or logical window. Without * size hints Chromium guesses 1280×720 when initializing hardware decoding. diff --git a/js/core/src/TerminalStore.ts b/js/core/src/TerminalStore.ts index 487d59e80..25078dcfd 100644 --- a/js/core/src/TerminalStore.ts +++ b/js/core/src/TerminalStore.ts @@ -1,12 +1,12 @@ import type { Terminal } from "@yas-run/browser"; -import { DEFAULT_FONT, DEFAULT_FONT_SIZE } from "./types"; -import type { ConnectionStatus, TerminalId, TerminalPalette } from "./types"; +import { DEFAULT_FONT, DEFAULT_FONT_SIZE } from "./types.js"; +import type { ConnectionStatus, TerminalId, TerminalPalette } from "./types.js"; import { createGlRenderer, createCanvas2dRenderer, type GlRenderer, -} from "./gl-renderer"; -import { createWebGpuRenderer } from "./webgpu-renderer"; +} from "./gl-renderer.js"; +import { createWebGpuRenderer } from "./webgpu-renderer.js"; const DISPLAY_FPS_SAMPLE_COUNT = 120; diff --git a/js/core/src/YasNativeWorkspaceConnection.ts b/js/core/src/YasNativeWorkspaceConnection.ts index b95fec79f..28680cf4b 100644 --- a/js/core/src/YasNativeWorkspaceConnection.ts +++ b/js/core/src/YasNativeWorkspaceConnection.ts @@ -2,19 +2,19 @@ import { detectSurfaceColorCapabilities, surfaceColorExtensions, onSurfaceColorChange, -} from "./surfaceColor"; -import { plannedDropExtension, plannedDropName } from "./surfaceDrop"; -import { AudioPlayer } from "./AudioPlayer"; -import { serverPlatform, type YasPlatform } from "./yas/core"; -import { DesktopStore } from "./desktopModel"; -import { EXIT_STATUS_UNKNOWN } from "./exit-status"; -import { MediaStore, MprisStore } from "./mediaModel"; -import { SurfaceStore, type SurfaceFrameAckToken } from "./SurfaceStore"; -import { TerminalStore, type YasWasmModule } from "./TerminalStore"; +} from "./surfaceColor.js"; +import { plannedDropExtension, plannedDropName } from "./surfaceDrop.js"; +import { AudioPlayer } from "./AudioPlayer.js"; +import { serverPlatform, type YasPlatform } from "./yas/core.js"; +import { DesktopStore } from "./desktopModel.js"; +import { EXIT_STATUS_UNKNOWN } from "./exit-status.js"; +import { MediaStore, MprisStore } from "./mediaModel.js"; +import { SurfaceStore, type SurfaceFrameAckToken } from "./SurfaceStore.js"; +import { TerminalStore, type YasWasmModule } from "./TerminalStore.js"; import { currentBrowserClipboardEpoch, noteBrowserClipboardMayHaveChanged as noteBrowserClipboardChange, -} from "./clipboardAuthority"; +} from "./clipboardAuthority.js"; import type { ConnectionId, ConnectionStatus, @@ -26,7 +26,7 @@ import type { YasSearchResult, YasSession, YasClientList, -} from "./types"; +} from "./types.js"; import { CODEC_SUPPORT_AV1, CODEC_SUPPORT_AV1_444, @@ -36,18 +36,18 @@ import { SURFACE_FRAME_CODEC_H264, SURFACE_FRAME_CODEC_PNG, SURFACE_FRAME_FLAG_KEYFRAME, -} from "./surfaceModel"; -import { getCodecSupport } from "./YasSurfaceCanvas"; +} from "./surfaceModel.js"; +import { getCodecSupport } from "./YasSurfaceCanvas.js"; import type { AwaitSessionExitOptions, CreateSessionOptions, SurfaceTarget, -} from "./workspaceConnectionTypes"; +} from "./workspaceConnectionTypes.js"; import type { SurfaceAxisEvent, SurfaceDragItem, SurfaceTouchPoint, -} from "./input"; +} from "./input.js"; import { YAS_FAMILY_SELECTION, YAS_FAMILY_FONT, @@ -156,58 +156,58 @@ import { YAS_STATUS_UNAVAILABLE, YAS_STATUS_UNSUPPORTED, YAS_TERMINAL_COPY_RANGE, -} from "./yas/generated"; -import * as yasGenerated from "./yas/generated"; -import { YasFontClient, YasFontProtocol } from "./yas/font"; -import { YasNativeDesktopClientLifecycle } from "./yas/nativeDesktopMedia"; +} from "./yas/generated.js"; +import * as yasGenerated from "./yas/generated.js"; +import { YasFontClient, YasFontProtocol } from "./yas/font.js"; +import { YasNativeDesktopClientLifecycle } from "./yas/nativeDesktopMedia.js"; import { YasNativeExtensionFacade, type YasNativeExtensionInstallRequest, -} from "./yas/nativeExtensionFacade"; -import type { YasExtensionRecord } from "./yas/extension"; +} from "./yas/nativeExtensionFacade.js"; +import type { YasExtensionRecord } from "./yas/extension.js"; import { YasNativeChannelFacade, type YasNativeChannelHandle, type YasNativeChannelNamesWatch, type YasNativeChannelOpenOptions, -} from "./yas/nativeChannelFacade"; -import { YasNativeProductFamilies } from "./yas/nativeProductFamilies"; -import { decodeSurfaceCodecPayload } from "./yas/packed"; +} from "./yas/nativeChannelFacade.js"; +import { YasNativeProductFamilies } from "./yas/nativeProductFamilies.js"; +import { decodeSurfaceCodecPayload } from "./yas/packed.js"; import { YasNativeWorkspaceFs, type YasNativeFsSyncHandle, type YasNativeFsSyncOptions, -} from "./yas/nativeWorkspaceFs"; +} from "./yas/nativeWorkspaceFs.js"; import { YasNativeWorkspaceGit, type YasNativeGitDiscoverOptions, type YasNativeGitFoundRepo, type YasNativeGitOpenOptions, type YasNativeGitRepoHandle, -} from "./yas/nativeWorkspaceGit"; +} from "./yas/nativeWorkspaceGit.js"; import { YasNativeWorkspaceLsp, type YasNativeLspHandle, type YasNativeLspOpenOptions, -} from "./yas/nativeWorkspaceLsp"; -import { YasNativeWorkspaceKv } from "./yas/nativeWorkspaceKv"; -import type { FsFileIndex, FsGrepOptions, FsGrepResult } from "./fsModel"; +} from "./yas/nativeWorkspaceLsp.js"; +import { YasNativeWorkspaceKv } from "./yas/nativeWorkspaceKv.js"; +import type { FsFileIndex, FsGrepOptions, FsGrepResult } from "./fsModel.js"; import type { WorkspaceSessionKvDeleteOptions, WorkspaceSessionKvPutOptions, WorkspaceSessionKvWatch, WorkspaceSessionKvWatchOptions, -} from "./workspaceSessionKv"; +} from "./workspaceSessionKv.js"; import { YasSelectionClient, selectionDragDropItemsExtension, type YasSelectionGet, type YasSelectionSlotRecord, -} from "./yas/selection"; +} from "./yas/selection.js"; import { YasConnection as NativeYasSession, type YasInvalidation, -} from "./yas/session"; +} from "./yas/session.js"; import { YasSurfaceClient, surfaceActivationRevision, @@ -219,7 +219,7 @@ import { type YasSurfaceFrame, type YasSurfaceRecord, type YasSurfaceView, -} from "./yas/surface"; +} from "./yas/surface.js"; import { YasTerminalClient, decodeTerminalGridV1, @@ -228,9 +228,9 @@ import { type YasTerminalRecord, type YasTerminalViewConfiguration, type YasTerminalView, -} from "./yas/terminal"; -import { encodeBrowserTerminalGrid } from "./yas/terminalRenderer"; -import { equalBytes, YasResultError, YasWriter } from "./yas/wire"; +} from "./yas/terminal.js"; +import { encodeBrowserTerminalGrid } from "./yas/terminalRenderer.js"; +import { equalBytes, YasResultError, YasWriter } from "./yas/wire.js"; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); diff --git a/js/core/src/YasSurfaceCanvas.ts b/js/core/src/YasSurfaceCanvas.ts index 464fa930b..957edab38 100644 --- a/js/core/src/YasSurfaceCanvas.ts +++ b/js/core/src/YasSurfaceCanvas.ts @@ -3,28 +3,28 @@ import { releaseSurfaceCanvas, surface2DContext, SurfaceHdrPresenter, -} from "./surfaceColor"; +} from "./surfaceColor.js"; -import { plannedDropExtension, plannedDropName } from "./surfaceDrop"; -import type { ConnectionId, SurfaceId, YasSurface } from "./types"; -import type { YasNativeFsSyncHandle } from "./yas/nativeWorkspaceFs"; +import { plannedDropExtension, plannedDropName } from "./surfaceDrop.js"; +import type { ConnectionId, SurfaceId, YasSurface } from "./types.js"; +import type { YasNativeFsSyncHandle } from "./yas/nativeWorkspaceFs.js"; import { CODEC_SUPPORT_H264, CODEC_SUPPORT_AV1, CODEC_SUPPORT_H264_444, CODEC_SUPPORT_AV1_444, -} from "./surfaceModel"; -import type { YasWorkspace } from "./YasWorkspace"; -import type { YasWorkspaceConnection } from "./YasWorkspace"; +} from "./surfaceModel.js"; +import type { YasWorkspace } from "./YasWorkspace.js"; +import type { YasWorkspaceConnection } from "./YasWorkspace.js"; import type { RemoteSurfaceInput, SurfaceCursorImage, SurfaceCursorRect, SurfaceFramePresentationSize, SurfaceTextInputEvent, -} from "./SurfaceStore"; -import { placeImeTarget } from "./imeTarget"; -import { av1LevelString } from "./videoCodec"; +} from "./SurfaceStore.js"; +import { placeImeTarget } from "./imeTarget.js"; +import { av1LevelString } from "./videoCodec.js"; import { SURFACE_POINTER_DOWN, SURFACE_POINTER_UP, @@ -37,7 +37,7 @@ import { SURFACE_TOUCH_DOWN, SURFACE_TOUCH_MOTION, SURFACE_TOUCH_UP, -} from "./input"; +} from "./input.js"; import { SCROLL_STOP_MS, WHEEL_DETENT_PX, @@ -45,16 +45,16 @@ import { WHEEL_LINES_PER_DETENT, WHEEL_MODE_LINE, WHEEL_MODE_PAGE, -} from "./wheel"; +} from "./wheel.js"; import { devicePixelBox, drawHalved, halve, halvings, octaveCeil, -} from "./downscale"; +} from "./downscale.js"; -export { av1LevelString } from "./videoCodec"; +export { av1LevelString } from "./videoCodec.js"; /** Cached codec support bitmask. Computed once, reused for all resize messages. */ let _codecSupport: number | null = null; @@ -1483,7 +1483,7 @@ export class YasSurfaceCanvas { private _hasPresentedFirstFrame = false; /** Cached store reference so we can keep the frame listener alive * even when the connection is temporarily unavailable. */ - private _store: import("./SurfaceStore").SurfaceStore | null = null; + private _store: import("./SurfaceStore.js").SurfaceStore | null = null; private _connection: YasWorkspaceConnection | null; private _releasingConnection = false; private _workspaceUnsub: (() => void) | null = null; @@ -2598,8 +2598,9 @@ export class YasSurfaceCanvas { /** Register this visible mount with the connection exactly once. */ private serverSubscribe( conn: YasWorkspaceConnection | null = this.getConn(), - store: import("./SurfaceStore").SurfaceStore | null = conn?.surfaceStore ?? - this._store, + store: + | import("./SurfaceStore.js").SurfaceStore + | null = conn?.surfaceStore ?? this._store, ): void { if ( !this._live || @@ -2780,7 +2781,9 @@ export class YasSurfaceCanvas { } /** Copy the shared backing canvas onto our visible canvas. */ - private presentFromStore(store: import("./SurfaceStore").SurfaceStore): void { + private presentFromStore( + store: import("./SurfaceStore.js").SurfaceStore, + ): void { const src = store.getCanvas(this._surfaceId); const canvas = this.canvas; if (!src || !canvas) return; diff --git a/js/core/src/YasTerminalSurface.ts b/js/core/src/YasTerminalSurface.ts index 69206bf62..7f0cd37a0 100644 --- a/js/core/src/YasTerminalSurface.ts +++ b/js/core/src/YasTerminalSurface.ts @@ -1,11 +1,15 @@ import type { Terminal } from "@yas-run/browser"; -import type { YasWorkspace } from "./YasWorkspace"; -import type { YasWorkspaceConnection } from "./YasWorkspace"; -import type { TerminalPalette, ConnectionStatus, SessionId } from "./types"; -import { DEFAULT_FONT, DEFAULT_FONT_SIZE, DEFAULT_TEXT_GAMMA } from "./types"; -import { cancelFrame, scheduleFrame } from "./frameScheduler"; -import { measureCell, cssFontFamily, type CellMetrics } from "./measure"; -import type { GlRenderer } from "./gl-renderer"; +import type { YasWorkspace } from "./YasWorkspace.js"; +import type { YasWorkspaceConnection } from "./YasWorkspace.js"; +import type { TerminalPalette, ConnectionStatus, SessionId } from "./types.js"; +import { + DEFAULT_FONT, + DEFAULT_FONT_SIZE, + DEFAULT_TEXT_GAMMA, +} from "./types.js"; +import { cancelFrame, scheduleFrame } from "./frameScheduler.js"; +import { measureCell, cssFontFamily, type CellMetrics } from "./measure.js"; +import type { GlRenderer } from "./gl-renderer.js"; import { keyToBytes, encoder, @@ -13,14 +17,14 @@ import { refreshKeyboardLayout, encodeTerminalText, REPORT_ALL, -} from "./keyboard"; -import { MOUSE_DOWN, MOUSE_UP, MOUSE_MOVE } from "./input"; -import { YAS_TERMINAL_WHEEL_SOURCE_FINGER } from "./yas/generated"; -import { assessUrl, openUrlSafely, type UrlAssessment } from "./urlSecurity"; -import { devicePixelBox, drawHalved, halve, halvings } from "./downscale"; -import { gridCaretRect, placeChip, placeImeTarget } from "./imeTarget"; -import { captureDelta } from "./prediction"; -import { WheelDetents, notchedRows } from "./wheel"; +} from "./keyboard.js"; +import { MOUSE_DOWN, MOUSE_UP, MOUSE_MOVE } from "./input.js"; +import { YAS_TERMINAL_WHEEL_SOURCE_FINGER } from "./yas/generated.js"; +import { assessUrl, openUrlSafely, type UrlAssessment } from "./urlSecurity.js"; +import { devicePixelBox, drawHalved, halve, halvings } from "./downscale.js"; +import { gridCaretRect, placeChip, placeImeTarget } from "./imeTarget.js"; +import { captureDelta } from "./prediction.js"; +import { WheelDetents, notchedRows } from "./wheel.js"; /** One screen row's slice of a hyperlink's extent, inclusive of both columns. */ interface LinkSegment { diff --git a/js/core/src/YasWorkspace.ts b/js/core/src/YasWorkspace.ts index 6627db509..9d0eeab42 100644 --- a/js/core/src/YasWorkspace.ts +++ b/js/core/src/YasWorkspace.ts @@ -1,6 +1,6 @@ -import { YasUplinkTransport } from "./transports/uplink"; -import { YasNativeWorkspaceConnection } from "./YasNativeWorkspaceConnection"; -import type { AwaitSessionExitOptions } from "./workspaceConnectionTypes"; +import { YasUplinkTransport } from "./transports/uplink.js"; +import { YasNativeWorkspaceConnection } from "./YasNativeWorkspaceConnection.js"; +import type { AwaitSessionExitOptions } from "./workspaceConnectionTypes.js"; import type { YasConnectionSnapshot, YasSearchResult, @@ -11,35 +11,35 @@ import type { SessionId, SurfaceId, TransportConfig, -} from "./types"; -import type { YasWasmModule } from "./TerminalStore"; -import type { FsFileIndex, FsGrepOptions, FsGrepResult } from "./fsModel"; +} from "./types.js"; +import type { YasWasmModule } from "./TerminalStore.js"; +import type { FsFileIndex, FsGrepOptions, FsGrepResult } from "./fsModel.js"; import type { YasNativeFsSyncHandle, YasNativeFsSyncOptions, -} from "./yas/nativeWorkspaceFs"; +} from "./yas/nativeWorkspaceFs.js"; import type { YasNativeGitDiscoverOptions, YasNativeGitFoundRepo, YasNativeGitOpenOptions, YasNativeGitRepoHandle, -} from "./yas/nativeWorkspaceGit"; +} from "./yas/nativeWorkspaceGit.js"; import type { WorkspaceSessionKvDeleteOptions, WorkspaceSessionKvPutOptions, WorkspaceSessionKvWatch, WorkspaceSessionKvWatchOptions, -} from "./workspaceSessionKv"; +} from "./workspaceSessionKv.js"; import type { YasNativeLspHandle, YasNativeLspOpenOptions, -} from "./yas/nativeWorkspaceLsp"; -import { createShareTransport } from "./transports/webrtc-share"; -import { YasActivityStore } from "./activity"; -import { YasConnection as NativeYasConnection } from "./yas/session"; -import { YasEdgeWebSocketTransport } from "./yas/edge"; -import { yasBrowserConnectionOptions } from "./yas/defaults"; -import { retainBrowserClipboardObserver } from "./clipboardAuthority"; +} from "./yas/nativeWorkspaceLsp.js"; +import { createShareTransport } from "./transports/webrtc-share.js"; +import { YasActivityStore } from "./activity.js"; +import { YasConnection as NativeYasConnection } from "./yas/session.js"; +import { YasEdgeWebSocketTransport } from "./yas/edge.js"; +import { yasBrowserConnectionOptions } from "./yas/defaults.js"; +import { retainBrowserClipboardObserver } from "./clipboardAuthority.js"; export interface AddYasConnectionOptions { id: ConnectionId; diff --git a/js/core/src/desktopModel.ts b/js/core/src/desktopModel.ts index bcca0d0ce..c56b5e47d 100644 --- a/js/core/src/desktopModel.ts +++ b/js/core/src/desktopModel.ts @@ -1,4 +1,4 @@ -import { Notifier, type ReactiveStore } from "./reactive"; +import { Notifier, type ReactiveStore } from "./reactive.js"; export const DESKTOP_SUBSCRIBE_TRAY = 1 << 0; export const DESKTOP_SUBSCRIBE_NOTIFICATIONS = 1 << 1; diff --git a/js/core/src/downscale.ts b/js/core/src/downscale.ts index 2534a1465..9b5d9b01b 100644 --- a/js/core/src/downscale.ts +++ b/js/core/src/downscale.ts @@ -1,4 +1,4 @@ -import { surface2DContext } from "./surfaceColor"; +import { surface2DContext } from "./surfaceColor.js"; /** * Box-filtered minification for canvases the browser would otherwise shrink * on its own. diff --git a/js/core/src/fsModel.ts b/js/core/src/fsModel.ts index b7ddffa9e..f0700e187 100644 --- a/js/core/src/fsModel.ts +++ b/js/core/src/fsModel.ts @@ -1,4 +1,4 @@ -import type { SessionId } from "./types"; +import type { SessionId } from "./types.js"; /** Presentation flags shared by the native FS client and Workspace UI. */ export const FS_ENTRY_TYPE_MASK = 0b11; diff --git a/js/core/src/gitModel.ts b/js/core/src/gitModel.ts index ccd618cb7..18a19dbe2 100644 --- a/js/core/src/gitModel.ts +++ b/js/core/src/gitModel.ts @@ -1,4 +1,4 @@ -import type { SessionId } from "./types"; +import type { SessionId } from "./types.js"; export const GIT_STATUS_OK = 0; export const GIT_STATUS_UNKNOWN_ID = 1; diff --git a/js/core/src/gl-renderer.ts b/js/core/src/gl-renderer.ts index 90503c417..b69b717e0 100644 --- a/js/core/src/gl-renderer.ts +++ b/js/core/src/gl-renderer.ts @@ -1,4 +1,4 @@ -import type { CellMetrics } from "./measure"; +import type { CellMetrics } from "./measure.js"; const MAX_BATCH_VERTS = 65532; const GLYPH_FLOATS_PER_VERT = 8; diff --git a/js/core/src/index.ts b/js/core/src/index.ts index 48e571c52..2bdfa9f6d 100644 --- a/js/core/src/index.ts +++ b/js/core/src/index.ts @@ -1,35 +1,39 @@ -export { YasWorkspace, consoleLogger, nullLogger } from "./YasWorkspace"; -export type { YasLogger, YasWorkspaceConnection } from "./YasWorkspace"; -export { YasNativeWorkspaceConnection } from "./YasNativeWorkspaceConnection"; -export { noteBrowserClipboardMayHaveChanged } from "./clipboardAuthority"; -export { YasNativeRelayTransport } from "./yas/nativeRelayTransport"; -export { YasActivityStore } from "./activity"; +export { YasWorkspace, consoleLogger, nullLogger } from "./YasWorkspace.js"; +export type { YasLogger, YasWorkspaceConnection } from "./YasWorkspace.js"; +export { YasNativeWorkspaceConnection } from "./YasNativeWorkspaceConnection.js"; +export { noteBrowserClipboardMayHaveChanged } from "./clipboardAuthority.js"; +export { YasNativeRelayTransport } from "./yas/nativeRelayTransport.js"; +export { YasActivityStore } from "./activity.js"; export type { YasActivity, YasActivityHandle, YasActivityUpdate, -} from "./activity"; +} from "./activity.js"; export { YAS_TERMINAL_CATALOG_SEARCH_SOURCE_TITLE as SEARCH_SOURCE_TITLE, YAS_TERMINAL_CATALOG_SEARCH_SOURCE_VISIBLE as SEARCH_SOURCE_VISIBLE, YAS_TERMINAL_CATALOG_SEARCH_SOURCE_SCROLLBACK as SEARCH_SOURCE_SCROLLBACK, YAS_STATUS_RESOURCE_EXHAUSTED, -} from "./yas/generated"; -export { YasResultError } from "./yas/wire"; +} from "./yas/generated.js"; +export { YasResultError } from "./yas/wire.js"; export type { AwaitSessionExitOptions, CreateSessionOptions, SurfaceTarget, -} from "./workspaceConnectionTypes"; +} from "./workspaceConnectionTypes.js"; -export type { YasWasmModule } from "./TerminalStore"; -export { AudioPlayer } from "./AudioPlayer"; +export type { YasWasmModule } from "./TerminalStore.js"; +export { AudioPlayer } from "./AudioPlayer.js"; export { releaseRecordingAudioSession, retainRecordingAudioSession, -} from "./audioSession"; -export { NumberRing, SurfaceFrameHistory, SurfaceStore } from "./SurfaceStore"; +} from "./audioSession.js"; +export { + NumberRing, + SurfaceFrameHistory, + SurfaceStore, +} from "./SurfaceStore.js"; export type { SurfaceFrameCallback, SurfaceEventCallback, @@ -41,36 +45,40 @@ export type { SurfaceCursorRect, SurfaceTextInputEvent, SurfaceTextInputState, -} from "./SurfaceStore"; +} from "./SurfaceStore.js"; export { estimateSourceToReceiveMs, sourceTimestampDelta, wrappingTimestampDelta, -} from "./SurfaceStore"; +} from "./SurfaceStore.js"; -export { clampZoom, driveSurfaceResize } from "./surfaceResize"; -export type { SurfaceResizeTarget, SurfaceZoom } from "./surfaceResize"; +export { clampZoom, driveSurfaceResize } from "./surfaceResize.js"; +export type { SurfaceResizeTarget, SurfaceZoom } from "./surfaceResize.js"; -export { measureCell, cssFontFamily } from "./measure"; -export type { CellMetrics } from "./measure"; +export { measureCell, cssFontFamily } from "./measure.js"; +export type { CellMetrics } from "./measure.js"; -export { assessUrl, escapeUrlForDisplay, openUrlSafely } from "./urlSecurity"; -export type { UrlAssessment, UrlVerdict, UrlReason } from "./urlSecurity"; +export { + assessUrl, + escapeUrlForDisplay, + openUrlSafely, +} from "./urlSecurity.js"; +export type { UrlAssessment, UrlVerdict, UrlReason } from "./urlSecurity.js"; -export { createShareTransport } from "./transports/webrtc-share"; +export { createShareTransport } from "./transports/webrtc-share.js"; /** YAS v1 session, Transfer, Relay, Font, and browser-edge clients. */ -export * from "./yas"; -export * from "./workspaceSessionKv"; +export * from "./yas/index.js"; +export * from "./workspaceSessionKv.js"; /** HTTP/1.1 over a relayed stream, for the preview service worker. */ -export * from "./http1"; +export * from "./http1.js"; /** Preview targets and the /x/ bootstrap prefix. */ -export * from "./preview"; +export * from "./preview.js"; /** Durable backend workspaces. */ -export * from "./workspaceSessions"; +export * from "./workspaceSessions.js"; /** Durable per-device workspace attachment ordering. */ -export * from "./workspaceSessionDevices"; +export * from "./workspaceSessionDevices.js"; // Product-model types and presentation helpers retained by the UI. export { @@ -85,7 +93,7 @@ export { TRAY_MENU_OK, TRAY_STATUS_NEEDS_ATTENTION, TRAY_STATUS_PASSIVE, -} from "./desktopModel"; +} from "./desktopModel.js"; export type { DesktopId, DesktopImage, @@ -94,7 +102,7 @@ export type { TrayItem, TrayMenu, TrayMenuNode, -} from "./desktopModel"; +} from "./desktopModel.js"; export { ACTIVE_CAMERA, ACTIVE_MICROPHONE, @@ -119,7 +127,7 @@ export { cameraCodecProbeReport, probeCameraCodecs, probeOpusMicrophone, -} from "./mediaModel"; +} from "./mediaModel.js"; export type { CameraCodecProbeOutcome, CameraQuality, @@ -130,7 +138,7 @@ export type { PortalChoiceValue, PortalRequest, ScreenCastState, -} from "./mediaModel"; +} from "./mediaModel.js"; export { FS_ENTRY_DIR, FS_ENTRY_FILE, @@ -140,13 +148,13 @@ export { FS_ENTRY_TYPE_MASK, FS_ENTRY_UNREADABLE, FS_ENTRY_UNSTABLE, -} from "./fsModel"; +} from "./fsModel.js"; export type { FsFileIndex, FsGrepFile, FsGrepOptions, FsGrepResult, -} from "./fsModel"; +} from "./fsModel.js"; export { GIT_CLOSED_CLIENT_REQUEST, GIT_CLOSED_CONNECTION_LOST, @@ -185,9 +193,9 @@ export { gitOidFromHex, gitOidHex, gitStatusText, -} from "./gitModel"; -export type { GitOid, GitPatchRecord, GitWorktreeRecord } from "./gitModel"; -export { GitStateMirror, GitStatusError } from "./gitModel"; +} from "./gitModel.js"; +export type { GitOid, GitPatchRecord, GitWorktreeRecord } from "./gitModel.js"; +export { GitStateMirror, GitStatusError } from "./gitModel.js"; export { LSP_COMPLETION_DEPRECATED, LSP_COMPLETION_PRESELECT, @@ -204,32 +212,36 @@ export { LSP_STATUS_OK, LSP_STATUS_WARMING, lspStatusText, -} from "./lspModel"; +} from "./lspModel.js"; export type { YasNativeChannelHandle as ChannelHandle, YasNativeChannelNamesWatch as ChannelNamesWatch, YasNativeChannelOpenOptions as ChannelOpenOptions, -} from "./yas/nativeChannelFacade"; -export type { NetOpenOptions, NetStream } from "./netModel"; -export { formatExtensionId, parseModuleDigest } from "./extensionModel"; +} from "./yas/nativeChannelFacade.js"; +export type { NetOpenOptions, NetStream } from "./netModel.js"; +export { formatExtensionId, parseModuleDigest } from "./extensionModel.js"; -export { DEFAULT_FONT, DEFAULT_FONT_SIZE, DEFAULT_TEXT_GAMMA } from "./types"; +export { + DEFAULT_FONT, + DEFAULT_FONT_SIZE, + DEFAULT_TEXT_GAMMA, +} from "./types.js"; export { CODEC_SUPPORT_H264, CODEC_SUPPORT_AV1, CODEC_SUPPORT_H264_444, CODEC_SUPPORT_AV1_444, -} from "./surfaceModel"; -export type { YasTransportMessage } from "./types"; +} from "./surfaceModel.js"; +export type { YasTransportMessage } from "./types.js"; export { EXIT_STATUS_UNKNOWN, exitCodeFromStatus, formatExitStatus, -} from "./exit-status"; +} from "./exit-status.js"; -export { Notifier } from "./reactive"; -export type { ReactiveStore } from "./reactive"; +export { Notifier } from "./reactive.js"; +export type { ReactiveStore } from "./reactive.js"; export type { YasConnectionSnapshot, @@ -254,7 +266,7 @@ export type { TerminalId, TerminalPalette, TransportConfig, -} from "./types"; +} from "./types.js"; export { SURFACE_POINTER_DOWN, @@ -262,26 +274,26 @@ export { SURFACE_POINTER_MOVE, CLIENT_DISCONNECT_REASON_MAX_BYTES, clientDisconnectReasonByteLength, -} from "./input"; +} from "./input.js"; -export { PALETTES } from "./palettes"; +export { PALETTES } from "./palettes.js"; -export { MOUSE_DOWN, MOUSE_UP, MOUSE_MOVE } from "./input"; -export { keyToBytes, ctrlCharToByte, encoder } from "./keyboard"; +export { MOUSE_DOWN, MOUSE_UP, MOUSE_MOVE } from "./input.js"; +export { keyToBytes, ctrlCharToByte, encoder } from "./keyboard.js"; -export type { GlRenderer, RendererBackend } from "./gl-renderer"; -export { createWebGpuRenderer } from "./webgpu-renderer"; +export type { GlRenderer, RendererBackend } from "./gl-renderer.js"; +export { createWebGpuRenderer } from "./webgpu-renderer.js"; export { YasTerminalSurface, isIOS, terminalSurfaceForInput, -} from "./YasTerminalSurface"; +} from "./YasTerminalSurface.js"; export type { YasTerminalSurfaceOptions, YasTerminalSurfaceHandle, LinkHover, -} from "./YasTerminalSurface"; +} from "./YasTerminalSurface.js"; export { YAS_SURFACE_TEXT_INPUT_EVENT, @@ -293,12 +305,12 @@ export { getProbedCodecSupport, setAllowedCodecSupport, getMaxDecodeSize, -} from "./YasSurfaceCanvas"; +} from "./YasSurfaceCanvas.js"; export type { YasSurfaceTextInputEvent, YasSurfaceCanvasOptions, SurfaceTouchMode, -} from "./YasSurfaceCanvas"; +} from "./YasSurfaceCanvas.js"; export { LAYOUT_MAX_DEPTH, @@ -307,13 +319,13 @@ export { validateLayoutNode, validateWorkspaceLayout, sameLayoutTree, -} from "./layout/model"; +} from "./layout/model.js"; export type { LayoutNode, LayoutSplit, LayoutChild, LayoutLeaf, -} from "./layout/model"; +} from "./layout/model.js"; export { enumeratePanes, @@ -321,17 +333,17 @@ export { buildCandidateOrder, reconcileAssignments, adjustWeights, -} from "./layout/tree"; +} from "./layout/tree.js"; export type { WorkspaceLayout, LayoutPane, LayoutAssignments, -} from "./layout/tree"; +} from "./layout/tree.js"; -export { YasUplinkTransport } from "./transports/uplink"; -export { YasNoiseTransport } from "./transports/noise"; -export type { YasNoiseTransportOptions } from "./transports/noise"; +export { YasUplinkTransport } from "./transports/uplink.js"; +export { YasNoiseTransport } from "./transports/noise.js"; +export type { YasNoiseTransportOptions } from "./transports/noise.js"; export { generateUplinkKeyPair, uplinkPublicKey, -} from "./transports/uplink-crypto"; +} from "./transports/uplink-crypto.js"; diff --git a/js/core/src/keyboard.ts b/js/core/src/keyboard.ts index bb0e9290f..0f67db0a6 100644 --- a/js/core/src/keyboard.ts +++ b/js/core/src/keyboard.ts @@ -5,12 +5,12 @@ import { REPORT_ALL, REPORT_EVENTS, type TerminalKey, -} from "./keyboardProtocol"; +} from "./keyboardProtocol.js"; export { encodeTerminalText, REPORT_ALL, REPORT_EVENTS, -} from "./keyboardProtocol"; +} from "./keyboardProtocol.js"; export const encoder = new TextEncoder(); export function ctrlCharToByte(char: string): Uint8Array | null { diff --git a/js/core/src/layout/index.ts b/js/core/src/layout/index.ts index 74dd64b85..ebb0813a2 100644 --- a/js/core/src/layout/index.ts +++ b/js/core/src/layout/index.ts @@ -5,7 +5,7 @@ export { sameLayoutTree, LAYOUT_MAX_DEPTH, LAYOUT_MAX_PANES, -} from "./model"; +} from "./model.js"; export type { LayoutNode, LayoutSplit, @@ -13,7 +13,7 @@ export type { LayoutLeaf, LayoutRect, LayoutDirection, -} from "./model"; +} from "./model.js"; export { enumeratePanes, @@ -41,11 +41,11 @@ export { isWebAssignment, parseWebAssignment, isContentAssignment, -} from "./tree"; +} from "./tree.js"; export type { WorkspaceLayout, LayoutPane, LayoutAssignments, TileAssignment, DiffSide, -} from "./tree"; +} from "./tree.js"; diff --git a/js/core/src/layout/tree.ts b/js/core/src/layout/tree.ts index e076c9056..0d75d7a6e 100644 --- a/js/core/src/layout/tree.ts +++ b/js/core/src/layout/tree.ts @@ -1,13 +1,13 @@ -import type { SurfaceId } from "../types"; +import type { SurfaceId } from "../types.js"; import type { LayoutNode, LayoutSplit, LayoutChild, LayoutLeaf, LayoutRect, -} from "./model"; +} from "./model.js"; -export type { WorkspaceLayout } from "./model"; +export type { WorkspaceLayout } from "./model.js"; export interface LayoutPane { id: string; diff --git a/js/core/src/lspModel.ts b/js/core/src/lspModel.ts index 6b1be1b8b..ede5fcc83 100644 --- a/js/core/src/lspModel.ts +++ b/js/core/src/lspModel.ts @@ -1,4 +1,4 @@ -import type { SessionId } from "./types"; +import type { SessionId } from "./types.js"; export const LSP_STATUS_OK = 0; export const LSP_STATUS_UNKNOWN_ID = 1; diff --git a/js/core/src/mediaModel.ts b/js/core/src/mediaModel.ts index a552bca27..f9319d771 100644 --- a/js/core/src/mediaModel.ts +++ b/js/core/src/mediaModel.ts @@ -1,10 +1,10 @@ import { releaseRecordingAudioSession, retainRecordingAudioSession, -} from "./audioSession"; -import { Notifier, type ReactiveStore } from "./reactive"; -import type { SurfaceId } from "./types"; -import { av1LevelString } from "./videoCodec"; +} from "./audioSession.js"; +import { Notifier, type ReactiveStore } from "./reactive.js"; +import type { SurfaceId } from "./types.js"; +import { av1LevelString } from "./videoCodec.js"; export const RUNTIME_PIPEWIRE = 1 << 0; export const RUNTIME_MICROPHONE = 1 << 1; diff --git a/js/core/src/node-wasm.ts b/js/core/src/node-wasm.ts index cac30ed43..dc1a1bf2f 100644 --- a/js/core/src/node-wasm.ts +++ b/js/core/src/node-wasm.ts @@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url"; import init from "@yas-run/browser"; -import type { YasWasmModule } from "./TerminalStore"; +import type { YasWasmModule } from "./TerminalStore.js"; /** * Initialise the `@yas-run/browser` WASM module in a non-browser runtime diff --git a/js/core/src/node.ts b/js/core/src/node.ts index 3e875ea45..40de8f400 100644 --- a/js/core/src/node.ts +++ b/js/core/src/node.ts @@ -10,9 +10,9 @@ * `@yas-run/core` / `@yas-run/core/transports` instead. */ -export { NodeUnixSocketTransport } from "./transports/unix"; -export { BunUnixSocketTransport } from "./transports/unix-bun"; -export { DenoUnixSocketTransport } from "./transports/unix-deno"; -export type { UnixSocketTransportOptions } from "./transports/unix-base"; +export { NodeUnixSocketTransport } from "./transports/unix.js"; +export { BunUnixSocketTransport } from "./transports/unix-bun.js"; +export { DenoUnixSocketTransport } from "./transports/unix-deno.js"; +export type { UnixSocketTransportOptions } from "./transports/unix-base.js"; -export { loadYasWasm } from "./node-wasm"; +export { loadYasWasm } from "./node-wasm.js"; diff --git a/js/core/src/palettes.ts b/js/core/src/palettes.ts index 81457658a..3bd3731b7 100644 --- a/js/core/src/palettes.ts +++ b/js/core/src/palettes.ts @@ -1,4 +1,4 @@ -import type { TerminalPalette } from "./types"; +import type { TerminalPalette } from "./types.js"; export const PALETTES: TerminalPalette[] = [ { diff --git a/js/core/src/surfaceColor.ts b/js/core/src/surfaceColor.ts index 914c50dec..dda68cec1 100644 --- a/js/core/src/surfaceColor.ts +++ b/js/core/src/surfaceColor.ts @@ -1,6 +1,6 @@ -import { SURFACE_HDR_PROBE } from "./surfaceHdrProbe"; -import type { YasSurfaceColorSpace } from "./yas/packed"; -import type { YasExtension } from "./yas/wire"; +import { SURFACE_HDR_PROBE } from "./surfaceHdrProbe.js"; +import type { YasSurfaceColorSpace } from "./yas/packed.js"; +import type { YasExtension } from "./yas/wire.js"; import { YAS_SURFACE_COLOR_CAP_DISPLAY_P3, YAS_SURFACE_COLOR_CAP_HDR10_AV1, @@ -8,7 +8,7 @@ import { YAS_SURFACE_COLOR_CAP_AV1_444, YAS_SURFACE_COLOR_CAP_H264_444, YAS_SURFACE_VIEW_COLOR_CAPABILITIES_EXTENSION, -} from "./yas/generated"; +} from "./yas/generated.js"; export const SDR_COLOR: VideoColorSpaceInit = { primaries: "bt709", diff --git a/js/core/src/transports/browser.ts b/js/core/src/transports/browser.ts index b878e5f39..392d69b36 100644 --- a/js/core/src/transports/browser.ts +++ b/js/core/src/transports/browser.ts @@ -1,14 +1,14 @@ -export { createWebRtcDataChannelTransport } from "./webrtc"; -export type { WebRtcDataChannelTransportOptions } from "./webrtc"; +export { createWebRtcDataChannelTransport } from "./webrtc.js"; +export type { WebRtcDataChannelTransportOptions } from "./webrtc.js"; -export { createShareTransport } from "./webrtc-share"; +export { createShareTransport } from "./webrtc-share.js"; -export { YasWebTransportTransport } from "./webtransport"; -export type { YasWebTransportOptions } from "./webtransport"; +export { YasWebTransportTransport } from "./webtransport.js"; +export type { YasWebTransportOptions } from "./webtransport.js"; -export type { UnixSocketTransportOptions } from "./unix-base"; +export type { UnixSocketTransportOptions } from "./unix-base.js"; -export { YasUplinkTransport } from "./uplink"; -export { YasNoiseTransport } from "./noise"; -export type { YasNoiseTransportOptions } from "./noise"; -export { generateUplinkKeyPair, uplinkPublicKey } from "./uplink-crypto"; +export { YasUplinkTransport } from "./uplink.js"; +export { YasNoiseTransport } from "./noise.js"; +export type { YasNoiseTransportOptions } from "./noise.js"; +export { generateUplinkKeyPair, uplinkPublicKey } from "./uplink-crypto.js"; diff --git a/js/core/src/transports/index.ts b/js/core/src/transports/index.ts index 922ae783d..9411a4f1b 100644 --- a/js/core/src/transports/index.ts +++ b/js/core/src/transports/index.ts @@ -1,17 +1,17 @@ -export { createWebRtcDataChannelTransport } from "./webrtc"; -export type { WebRtcDataChannelTransportOptions } from "./webrtc"; +export { createWebRtcDataChannelTransport } from "./webrtc.js"; +export type { WebRtcDataChannelTransportOptions } from "./webrtc.js"; -export { createShareTransport } from "./webrtc-share"; +export { createShareTransport } from "./webrtc-share.js"; -export { YasWebTransportTransport } from "./webtransport"; -export type { YasWebTransportOptions } from "./webtransport"; +export { YasWebTransportTransport } from "./webtransport.js"; +export type { YasWebTransportOptions } from "./webtransport.js"; -export { NodeUnixSocketTransport } from "./unix"; -export { BunUnixSocketTransport } from "./unix-bun"; -export { DenoUnixSocketTransport } from "./unix-deno"; -export type { UnixSocketTransportOptions } from "./unix-base"; +export { NodeUnixSocketTransport } from "./unix.js"; +export { BunUnixSocketTransport } from "./unix-bun.js"; +export { DenoUnixSocketTransport } from "./unix-deno.js"; +export type { UnixSocketTransportOptions } from "./unix-base.js"; -export { YasUplinkTransport } from "./uplink"; -export { YasNoiseTransport } from "./noise"; -export type { YasNoiseTransportOptions } from "./noise"; -export { generateUplinkKeyPair, uplinkPublicKey } from "./uplink-crypto"; +export { YasUplinkTransport } from "./uplink.js"; +export { YasNoiseTransport } from "./noise.js"; +export type { YasNoiseTransportOptions } from "./noise.js"; +export { generateUplinkKeyPair, uplinkPublicKey } from "./uplink-crypto.js"; diff --git a/js/core/src/transports/noise.ts b/js/core/src/transports/noise.ts index 750c0ecc4..dab69b4ec 100644 --- a/js/core/src/transports/noise.ts +++ b/js/core/src/transports/noise.ts @@ -4,8 +4,8 @@ import type { YasTransportEventMap, YasTransportMessage, YasTransportOptions, -} from "../types"; -import { YAS_EVENT_HEADER_BYTES } from "../yas/generated"; +} from "../types.js"; +import { YAS_EVENT_HEADER_BYTES } from "../yas/generated.js"; import { type Bytes, NoiseCipher, @@ -19,7 +19,7 @@ import { equal, importIdentity, publicKey, -} from "./uplink-crypto"; +} from "./uplink-crypto.js"; const QUEUE_LIMIT = 4 * 1024 * 1024; const DATAGRAM_IN_FLIGHT = 64; diff --git a/js/core/src/transports/unix-base.ts b/js/core/src/transports/unix-base.ts index d7996caa5..7894f2551 100644 --- a/js/core/src/transports/unix-base.ts +++ b/js/core/src/transports/unix-base.ts @@ -2,7 +2,7 @@ import type { YasTransport, YasTransportOptions, ConnectionStatus, -} from "../types"; +} from "../types.js"; export interface UnixSocketTransportOptions extends YasTransportOptions {} diff --git a/js/core/src/transports/unix-bun.ts b/js/core/src/transports/unix-bun.ts index a207e3bd8..0d80637a8 100644 --- a/js/core/src/transports/unix-bun.ts +++ b/js/core/src/transports/unix-bun.ts @@ -3,9 +3,9 @@ import { AbstractUnixSocketTransport, type UnixSocketTransportOptions, -} from "./unix-base"; +} from "./unix-base.js"; -export type { UnixSocketTransportOptions } from "./unix-base"; +export type { UnixSocketTransportOptions } from "./unix-base.js"; type BunSocket = import("bun").Socket<{ attempt: symbol }>; diff --git a/js/core/src/transports/unix-deno.ts b/js/core/src/transports/unix-deno.ts index 9c25e6ffc..8ff02ef10 100644 --- a/js/core/src/transports/unix-deno.ts +++ b/js/core/src/transports/unix-deno.ts @@ -1,9 +1,9 @@ import { AbstractUnixSocketTransport, type UnixSocketTransportOptions, -} from "./unix-base"; +} from "./unix-base.js"; -export type { UnixSocketTransportOptions } from "./unix-base"; +export type { UnixSocketTransportOptions } from "./unix-base.js"; // Minimal structural typing for the subset of the Deno global we use, so // this file compiles under Node/Bun tsconfigs without @types/deno. diff --git a/js/core/src/transports/unix.ts b/js/core/src/transports/unix.ts index 9d72df0d6..8450fb1e4 100644 --- a/js/core/src/transports/unix.ts +++ b/js/core/src/transports/unix.ts @@ -3,9 +3,9 @@ import { connect as netConnect, type Socket } from "node:net"; import { AbstractUnixSocketTransport, type UnixSocketTransportOptions, -} from "./unix-base"; +} from "./unix-base.js"; -export type { UnixSocketTransportOptions } from "./unix-base"; +export type { UnixSocketTransportOptions } from "./unix-base.js"; /** * Node.js unix-domain-socket transport for `yas server` IPC. diff --git a/js/core/src/transports/uplink.ts b/js/core/src/transports/uplink.ts index b625c79d4..93b17000c 100644 --- a/js/core/src/transports/uplink.ts +++ b/js/core/src/transports/uplink.ts @@ -1,6 +1,6 @@ -import type { YasTransportOptions } from "../types"; -import { UplinkEvents, YasNoiseTransport } from "./noise"; -import { decodeKey, publicKey } from "./uplink-crypto"; +import type { YasTransportOptions } from "../types.js"; +import { UplinkEvents, YasNoiseTransport } from "./noise.js"; +import { decodeKey, publicKey } from "./uplink-crypto.js"; interface UplinkTarget { attach: string; diff --git a/js/core/src/transports/webrtc-share.ts b/js/core/src/transports/webrtc-share.ts index e9cca2520..3bbb0c603 100644 --- a/js/core/src/transports/webrtc-share.ts +++ b/js/core/src/transports/webrtc-share.ts @@ -8,14 +8,14 @@ */ import nacl from "tweetnacl"; -import { createWebRtcDataChannelTransport } from "./webrtc"; +import { createWebRtcDataChannelTransport } from "./webrtc.js"; import { noopDebug, type YasDebug, type YasTransport, type YasTransportMessage, type ConnectionStatus, -} from "../types"; +} from "../types.js"; const PBKDF2_ROUNDS = 100_000; // The producer gathers its non-trickle candidates before answering (up to diff --git a/js/core/src/transports/webrtc.ts b/js/core/src/transports/webrtc.ts index d99cbdb0d..4abc43e2f 100644 --- a/js/core/src/transports/webrtc.ts +++ b/js/core/src/transports/webrtc.ts @@ -2,7 +2,7 @@ import type { YasTransport, YasTransportOptions, ConnectionStatus, -} from "../types"; +} from "../types.js"; export interface WebRtcDataChannelTransportOptions extends YasTransportOptions { /** Data channel label. Default: the canonical YAS v1 selector. */ diff --git a/js/core/src/transports/webtransport.ts b/js/core/src/transports/webtransport.ts index 2e69eea3b..0e841084d 100644 --- a/js/core/src/transports/webtransport.ts +++ b/js/core/src/transports/webtransport.ts @@ -4,7 +4,7 @@ import type { YasTransportEventMap, YasTransportMessage, YasTransportOptions, -} from "../types"; +} from "../types.js"; const YAS_TRANSPORT_DATAGRAM_HARD_MAX = 65_536; const AUTH_MAX_BYTES = 4_096; diff --git a/js/core/src/webgpu-renderer.ts b/js/core/src/webgpu-renderer.ts index 9b1ade754..278d9ddfd 100644 --- a/js/core/src/webgpu-renderer.ts +++ b/js/core/src/webgpu-renderer.ts @@ -1,5 +1,5 @@ -import type { CellMetrics } from "./measure"; -import { rgbLuma, type GlRenderer } from "./gl-renderer"; +import type { CellMetrics } from "./measure.js"; +import { rgbLuma, type GlRenderer } from "./gl-renderer.js"; // --------------------------------------------------------------------------- // WGSL shaders diff --git a/js/core/src/workspaceConnectionTypes.ts b/js/core/src/workspaceConnectionTypes.ts index e8f9b76cc..f97ace50e 100644 --- a/js/core/src/workspaceConnectionTypes.ts +++ b/js/core/src/workspaceConnectionTypes.ts @@ -1,4 +1,4 @@ -import type { SessionId } from "./types"; +import type { SessionId } from "./types.js"; export interface CreateSessionOptions { rows: number; diff --git a/js/core/src/workspaceSessionDevices.ts b/js/core/src/workspaceSessionDevices.ts index c4383cee4..2ee6edd63 100644 --- a/js/core/src/workspaceSessionDevices.ts +++ b/js/core/src/workspaceSessionDevices.ts @@ -9,7 +9,7 @@ import { type WorkspaceSessionStoreStatus, isWorkspaceSessionId, workspaceSessionKey, -} from "./workspaceSessions"; +} from "./workspaceSessions.js"; import { WorkspaceSessionKvConflictError, copyWorkspaceSessionHash, @@ -17,9 +17,9 @@ import { type WorkspaceSessionHash, type WorkspaceSessionKvEntry, type WorkspaceSessionKvWatch, -} from "./workspaceSessionKv"; -import { YasNativeWorkspaceKv } from "./yas/nativeWorkspaceKv"; -import type { YasConnection } from "./yas/session"; +} from "./workspaceSessionKv.js"; +import { YasNativeWorkspaceKv } from "./yas/nativeWorkspaceKv.js"; +import type { YasConnection } from "./yas/session.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8", { fatal: true }); diff --git a/js/core/src/workspaceSessions.ts b/js/core/src/workspaceSessions.ts index 723c30858..5100dd43b 100644 --- a/js/core/src/workspaceSessions.ts +++ b/js/core/src/workspaceSessions.ts @@ -6,21 +6,21 @@ import { validateWorkspaceLayout, type LayoutRect, type WorkspaceLayout, -} from "./layout/model"; +} from "./layout/model.js"; import type { WorkspaceSessionHash, WorkspaceSessionKv, WorkspaceSessionKvEntry, WorkspaceSessionKvWatch, WorkspaceSessionOwnedKv, -} from "./workspaceSessionKv"; +} from "./workspaceSessionKv.js"; import { WorkspaceSessionKvConflictError, copyWorkspaceSessionHash, workspaceSessionHashesEqual, -} from "./workspaceSessionKv"; -import { YasNativeWorkspaceKv } from "./yas/nativeWorkspaceKv"; -import type { YasConnection } from "./yas/session"; +} from "./workspaceSessionKv.js"; +import { YasNativeWorkspaceKv } from "./yas/nativeWorkspaceKv.js"; +import type { YasConnection } from "./yas/session.js"; export type { WorkspaceSessionHash, @@ -32,8 +32,8 @@ export type { WorkspaceSessionKvWatch, WorkspaceSessionKvWatchOptions, WorkspaceSessionOwnedKv, -} from "./workspaceSessionKv"; -export { WorkspaceSessionKvConflictError } from "./workspaceSessionKv"; +} from "./workspaceSessionKv.js"; +export { WorkspaceSessionKvConflictError } from "./workspaceSessionKv.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8", { fatal: true }); diff --git a/js/core/src/yas/channel.ts b/js/core/src/yas/channel.ts index 0011272cb..9eb0a2013 100644 --- a/js/core/src/yas/channel.ts +++ b/js/core/src/yas/channel.ts @@ -21,8 +21,8 @@ import { YAS_FAMILY_CHANNEL, YAS_FAMILY_TRANSFER, YAS_TRANSFER_RESET, -} from "./generated"; -import type { YasConnection } from "./session"; +} from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -38,7 +38,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_MESSAGE, YAS_TRANSFER_RECEIVER_TO_SENDER, @@ -48,7 +48,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YAS_STATUS_CANCELLED, YAS_STATUS_RESOURCE_EXHAUSTED, @@ -61,7 +61,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_CHANNEL_ACCEPT, @@ -75,7 +75,7 @@ export { YAS_CHANNEL_UNWATCH, YAS_CHANNEL_VERSION, YAS_CHANNEL_WATCH, -} from "./generated"; +} from "./generated.js"; export interface YasChannelListenerRecord { listenerHandle: bigint; diff --git a/js/core/src/yas/client.ts b/js/core/src/yas/client.ts index 752b7f561..b429e37f3 100644 --- a/js/core/src/yas/client.ts +++ b/js/core/src/yas/client.ts @@ -19,8 +19,8 @@ import { YAS_CLIENT_VERSION, YAS_CLIENT_WATCH, YAS_FAMILY_CLIENT, -} from "./generated"; -import type { YasConnection } from "./session"; +} from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -38,7 +38,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YasCursor, YasProtocolError, @@ -47,7 +47,7 @@ import { decodeTypedRecord, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_CLIENT_ACTIVE_SUBSCRIPTIONS_EXTENSION, @@ -68,7 +68,7 @@ export { YAS_CLIENT_VERSION, YAS_CLIENT_WATCH, YAS_FAMILY_CLIENT, -} from "./generated"; +} from "./generated.js"; export type YasClientOrigin = | { diff --git a/js/core/src/yas/core.ts b/js/core/src/yas/core.ts index d5a8d9b80..d2f135644 100644 --- a/js/core/src/yas/core.ts +++ b/js/core/src/yas/core.ts @@ -13,7 +13,7 @@ import { encodeExtensions, validateExtensionBody, type YasExtension, -} from "./wire"; +} from "./wire.js"; import { YAS_CORE_CANCEL, YAS_CORE_CLIENT_UPDATE, @@ -49,7 +49,7 @@ import { YAS_OPERATION_DIRECTION_MASKS, YAS_RELAY_LIMIT_MAX_LINKS_PER_SESSION, YAS_RELAY_LIMIT_MAX_PENDING_CONNECTS, -} from "./generated"; +} from "./generated.js"; export { YAS_CORE_CANCEL, @@ -66,7 +66,7 @@ export { YAS_FAMILY_FONT, YAS_FAMILY_RELAY, YAS_FAMILY_TRANSFER, -} from "./generated"; +} from "./generated.js"; export const YAS_RUNTIME_AVAILABLE = YAS_CORE_RUNTIME_AVAILABLE; export const YAS_RUNTIME_DEGRADED = YAS_CORE_RUNTIME_DEGRADED; diff --git a/js/core/src/yas/datagram.ts b/js/core/src/yas/datagram.ts index 6eab5b1ac..9b4f7bbf0 100644 --- a/js/core/src/yas/datagram.ts +++ b/js/core/src/yas/datagram.ts @@ -15,11 +15,11 @@ import { YAS_SURFACE_FRAME_DISCARDABLE, YAS_SURFACE_FRAME_END_OF_STREAM, YAS_SURFACE_FRAME_KEYFRAME, -} from "./generated"; -import { decodeMediaFrame, type YasMediaFrame } from "./media"; -import { decodeNetDatagram, type YasNetDatagram } from "./net"; -import { decodeSurfaceFrame, type YasSurfaceFrame } from "./surface"; -import { YasProtocolError, yasOperationPolicy, type YasFrame } from "./wire"; +} from "./generated.js"; +import { decodeMediaFrame, type YasMediaFrame } from "./media.js"; +import { decodeNetDatagram, type YasNetDatagram } from "./net.js"; +import { decodeSurfaceFrame, type YasSurfaceFrame } from "./surface.js"; +import { YasProtocolError, yasOperationPolicy, type YasFrame } from "./wire.js"; export type YasValidatedDatagram = | { diff --git a/js/core/src/yas/defaults.ts b/js/core/src/yas/defaults.ts index d306870ac..be02d2f18 100644 --- a/js/core/src/yas/defaults.ts +++ b/js/core/src/yas/defaults.ts @@ -39,8 +39,8 @@ import { YAS_FAMILY_SURFACE, YAS_FAMILY_TERMINAL, YAS_FAMILY_TRANSFER, -} from "./generated"; -import type { YasConnectionOptions } from "./session"; +} from "./generated.js"; +import type { YasConnectionOptions } from "./session.js"; /** * Browser receive inventory. Surface views keep a 16-frame window so a 120 Hz diff --git a/js/core/src/yas/desktop.ts b/js/core/src/yas/desktop.ts index 1f9a9ae0b..4f74efb00 100644 --- a/js/core/src/yas/desktop.ts +++ b/js/core/src/yas/desktop.ts @@ -1,7 +1,7 @@ /** YAS Desktop family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -19,7 +19,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_SENDER_TO_RECEIVER, @@ -27,7 +27,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -36,7 +36,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; const desktopNotificationExtensionTags: ReadonlySet = new Set([ g.YAS_DESKTOP_NOTIFICATION_IMAGE_HASH_EXTENSION, @@ -104,7 +104,7 @@ export { YAS_DESKTOP_WATCH_NOTIFICATIONS, YAS_DESKTOP_WATCH_TRAY, YAS_FAMILY_DESKTOP, -} from "./generated"; +} from "./generated.js"; export interface YasDesktopMenuNode { nodeHandle: bigint; diff --git a/js/core/src/yas/edge.ts b/js/core/src/yas/edge.ts index 0225525e9..ce808cf46 100644 --- a/js/core/src/yas/edge.ts +++ b/js/core/src/yas/edge.ts @@ -3,13 +3,13 @@ import type { YasTransportMessage, YasTransportOptions, ConnectionStatus, -} from "../types"; +} from "../types.js"; import { YasConnection, type YasConnectionOptions, type YasTransport, -} from "./session"; -import { YAS_WEBSOCKET_SUBPROTOCOL } from "./generated"; +} from "./session.js"; +import { YAS_WEBSOCKET_SUBPROTOCOL } from "./generated.js"; /** Exact browser contract for `/edge`: authenticate, then one YAS frame/message. */ export class YasEdgeWebSocketTransport implements YasTransport { diff --git a/js/core/src/yas/env.ts b/js/core/src/yas/env.ts index 0aceebc73..84376d918 100644 --- a/js/core/src/yas/env.ts +++ b/js/core/src/yas/env.ts @@ -14,8 +14,8 @@ import { YAS_ENV_VERSION, YAS_FAMILY_ENV, YAS_TRANSFER_SENSITIVE_CONTENT_EXTENSION, -} from "./generated"; -import type { YasConnection } from "./session"; +} from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_TRANSFER_MODE_MESSAGE, YAS_TRANSFER_SENDER_TO_RECEIVER, @@ -23,7 +23,7 @@ import { encodeTransferDescriptor, transfersFor, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -31,7 +31,7 @@ import { decodeExtensions, encodeExtensions, type YasExtension, -} from "./wire"; +} from "./wire.js"; export { YAS_ENV_DELIVERY_INLINE, @@ -45,7 +45,7 @@ export { YAS_ENV_MAX_VALUE_BYTES, YAS_ENV_SNAPSHOT_CONTENT_KIND, YAS_ENV_VERSION, -} from "./generated"; +} from "./generated.js"; export interface YasEnvEntry { key: Uint8Array; diff --git a/js/core/src/yas/events.ts b/js/core/src/yas/events.ts index d5a2a6d04..024446e3d 100644 --- a/js/core/src/yas/events.ts +++ b/js/core/src/yas/events.ts @@ -1,7 +1,7 @@ /** YAS process-wide binary event-journal family v1. */ -import * as g from "./generated"; -import type { YasConnection } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_SENDER_TO_RECEIVER, @@ -10,7 +10,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YAS_MAX_DECODED_FRAME, YasCursor, @@ -22,7 +22,7 @@ import { encodeExtensions, equalBytes, type YasExtension, -} from "./wire"; +} from "./wire.js"; export { YAS_EVENTS_DUMP, @@ -38,7 +38,7 @@ export { YAS_EVENTS_STREAM_STOPPED, YAS_EVENTS_VERSION, YAS_FAMILY_EVENTS, -} from "./generated"; +} from "./generated.js"; export interface YasEventsConfig { revision: bigint; diff --git a/js/core/src/yas/extension.ts b/js/core/src/yas/extension.ts index 39d35966f..e9a195cbf 100644 --- a/js/core/src/yas/extension.ts +++ b/js/core/src/yas/extension.ts @@ -1,7 +1,7 @@ /** YAS extension-supervisor family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -17,7 +17,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_MODE_MESSAGE, @@ -29,7 +29,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -39,7 +39,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_EXTENSION_ATTEMPT_CONTEXT, @@ -55,7 +55,7 @@ export { YAS_EXTENSION_VERSION, YAS_EXTENSION_WATCH, YAS_FAMILY_EXTENSION, -} from "./generated"; +} from "./generated.js"; export interface YasExtensionRuntimeLimits { memoryBytes: bigint; diff --git a/js/core/src/yas/font.ts b/js/core/src/yas/font.ts index 620748dd7..4daac4b50 100644 --- a/js/core/src/yas/font.ts +++ b/js/core/src/yas/font.ts @@ -1,4 +1,4 @@ -import { YAS_FAMILY_FONT } from "./core"; +import { YAS_FAMILY_FONT } from "./core.js"; import { YAS_FONT_DESCRIBE, YAS_FONT_DELIVERY_INLINE, @@ -39,8 +39,8 @@ import { YAS_FONT_UNWATCH, YAS_FONT_VERSION, YAS_FONT_WATCH, -} from "./generated"; -import type { YasConnection } from "./session"; +} from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -56,13 +56,13 @@ import { estimateStateRetainedBytes, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_SENDER_TO_RECEIVER, decodeTransferDescriptor, transfersFor, -} from "./transfer"; +} from "./transfer.js"; import { equalBytes, YasCursor, @@ -72,7 +72,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_FONT_DESCRIBE, @@ -94,7 +94,7 @@ export { YAS_FONT_UNWATCH, YAS_FONT_VERSION, YAS_FONT_WATCH, -} from "./generated"; +} from "./generated.js"; export const YAS_FONT_FORMAT_TRUETYPE = YAS_FONT_FORMAT_SFNT_TRUETYPE; export const YAS_FONT_FORMAT_CFF = YAS_FONT_FORMAT_SFNT_CFF; diff --git a/js/core/src/yas/fs.ts b/js/core/src/yas/fs.ts index 8c12cf26c..29350a690 100644 --- a/js/core/src/yas/fs.ts +++ b/js/core/src/yas/fs.ts @@ -1,7 +1,7 @@ /** YAS FS family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -20,7 +20,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_MODE_MESSAGE, @@ -35,7 +35,7 @@ import { type YasInlineOrTransfer, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -44,7 +44,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_FAMILY_FS, @@ -63,7 +63,7 @@ export { YAS_FS_UNWATCH, YAS_FS_VERSION, YAS_FS_WATCH, -} from "./generated"; +} from "./generated.js"; export interface YasFsPath { components: readonly Uint8Array[]; diff --git a/js/core/src/yas/git.ts b/js/core/src/yas/git.ts index 1043adf86..673ff4fe7 100644 --- a/js/core/src/yas/git.ts +++ b/js/core/src/yas/git.ts @@ -1,8 +1,8 @@ /** YAS Git family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection, YasReceiveBudgetLease } from "./session"; -import { decodeFsPath, encodeFsPath, type YasFsPath } from "./fs"; +import * as g from "./generated.js"; +import type { YasConnection, YasReceiveBudgetLease } from "./session.js"; +import { decodeFsPath, encodeFsPath, type YasFsPath } from "./fs.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -21,7 +21,7 @@ import { estimateStateRetainedBytes, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_MODE_MESSAGE, @@ -31,7 +31,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -41,7 +41,7 @@ import { encodeTypedRecord, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_FAMILY_GIT, @@ -60,7 +60,7 @@ export { YAS_GIT_VERSION, YAS_GIT_WATCH, YAS_GIT_WATCH_QUERY, -} from "./generated"; +} from "./generated.js"; export interface YasGitObjectId { algorithm: number; diff --git a/js/core/src/yas/index.ts b/js/core/src/yas/index.ts index bdc30f2d6..89d1ff804 100644 --- a/js/core/src/yas/index.ts +++ b/js/core/src/yas/index.ts @@ -1,36 +1,36 @@ -export * from "./generated"; -export * from "./wire"; -export * from "./core"; -export * from "./env"; -export * from "./kv"; -export * from "./session"; -export * from "./transfer"; -export * from "./state"; -export * from "./relay"; -export * from "./font"; -export * from "./terminal"; -export * from "./recording"; -export * from "./client"; -export * from "./channel"; -export * from "./surface"; -export * from "./selection"; -export * from "./desktop"; -export * from "./media"; -export * from "./process"; -export * from "./net"; -export * from "./datagram"; -export * from "./fs"; -export * from "./events"; -export * from "./lsp"; -export * from "./git"; -export * from "./extension"; -export * from "./packed"; -export * from "./defaults"; -export * from "./edge"; -export * from "./nativeProductFamilies"; -export * from "./nativeWorkspaceFs"; -export * from "./nativeWorkspaceGit"; -export * from "./nativeWorkspaceLsp"; -export * from "./nativeWorkspaceKv"; -export * from "./nativeChannelFacade"; -export * from "./nativeExtensionFacade"; +export * from "./generated.js"; +export * from "./wire.js"; +export * from "./core.js"; +export * from "./env.js"; +export * from "./kv.js"; +export * from "./session.js"; +export * from "./transfer.js"; +export * from "./state.js"; +export * from "./relay.js"; +export * from "./font.js"; +export * from "./terminal.js"; +export * from "./recording.js"; +export * from "./client.js"; +export * from "./channel.js"; +export * from "./surface.js"; +export * from "./selection.js"; +export * from "./desktop.js"; +export * from "./media.js"; +export * from "./process.js"; +export * from "./net.js"; +export * from "./datagram.js"; +export * from "./fs.js"; +export * from "./events.js"; +export * from "./lsp.js"; +export * from "./git.js"; +export * from "./extension.js"; +export * from "./packed.js"; +export * from "./defaults.js"; +export * from "./edge.js"; +export * from "./nativeProductFamilies.js"; +export * from "./nativeWorkspaceFs.js"; +export * from "./nativeWorkspaceGit.js"; +export * from "./nativeWorkspaceLsp.js"; +export * from "./nativeWorkspaceKv.js"; +export * from "./nativeChannelFacade.js"; +export * from "./nativeExtensionFacade.js"; diff --git a/js/core/src/yas/kv.ts b/js/core/src/yas/kv.ts index 578d99ef5..db452000c 100644 --- a/js/core/src/yas/kv.ts +++ b/js/core/src/yas/kv.ts @@ -1,7 +1,7 @@ /** YAS persistent key/value family codecs and client primitives. */ -import * as g from "./generated"; -import type { YasConnection } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_PATCH, @@ -11,7 +11,7 @@ import { encodeWatch, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { decodeInlineOrTransfer, decodeTransferDescriptor, @@ -21,7 +21,7 @@ import { transfersFor, type YasInlineOrTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -31,7 +31,7 @@ import { encodeExtensions, equalBytes, type YasExtension, -} from "./wire"; +} from "./wire.js"; export interface YasKvOpen { prefix: Uint8Array; diff --git a/js/core/src/yas/lsp.ts b/js/core/src/yas/lsp.ts index 85fb528c0..73099e69f 100644 --- a/js/core/src/yas/lsp.ts +++ b/js/core/src/yas/lsp.ts @@ -1,8 +1,8 @@ /** YAS LSP family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection } from "./session"; -import { decodeFsPath, encodeFsPath, type YasFsPath } from "./fs"; +import * as g from "./generated.js"; +import type { YasConnection } from "./session.js"; +import { decodeFsPath, encodeFsPath, type YasFsPath } from "./fs.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -19,7 +19,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_MODE_MESSAGE, @@ -31,7 +31,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -41,7 +41,7 @@ import { encodeTypedRecord, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_FAMILY_LSP, @@ -59,7 +59,7 @@ export { YAS_LSP_UNWATCH, YAS_LSP_VERSION, YAS_LSP_WATCH, -} from "./generated"; +} from "./generated.js"; export interface YasLspPosition { line: number; diff --git a/js/core/src/yas/media.ts b/js/core/src/yas/media.ts index 8e19282a9..6df95584d 100644 --- a/js/core/src/yas/media.ts +++ b/js/core/src/yas/media.ts @@ -1,7 +1,7 @@ /** YAS Media family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -19,7 +19,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_SENDER_TO_RECEIVER, @@ -27,7 +27,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -36,7 +36,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder("utf-8", { fatal: true }); @@ -61,7 +61,7 @@ export { YAS_MEDIA_UNWATCH, YAS_MEDIA_VERSION, YAS_MEDIA_WATCH, -} from "./generated"; +} from "./generated.js"; export interface YasMediaFormat { codec: number; diff --git a/js/core/src/yas/nativeChannelFacade.ts b/js/core/src/yas/nativeChannelFacade.ts index e32b54f2e..254496d51 100644 --- a/js/core/src/yas/nativeChannelFacade.ts +++ b/js/core/src/yas/nativeChannelFacade.ts @@ -13,17 +13,17 @@ import { YAS_STATUS_INVALID, YAS_STATUS_OK, YAS_STATUS_UNAVAILABLE, -} from "./generated"; +} from "./generated.js"; import { type YasChannelConnection, type YasChannelListenerRecord, type YasChannelSnapshot, YasChannelClient, -} from "./channel"; -import type { YasConnection } from "./session"; -import type { YasWatchOptions } from "./state"; -import type { YasTransfer } from "./transfer"; -import { YasProtocolError, YasResultError } from "./wire"; +} from "./channel.js"; +import type { YasConnection } from "./session.js"; +import type { YasWatchOptions } from "./state.js"; +import type { YasTransfer } from "./transfer.js"; +import { YasProtocolError, YasResultError } from "./wire.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8", { fatal: false }); diff --git a/js/core/src/yas/nativeDesktopMedia.ts b/js/core/src/yas/nativeDesktopMedia.ts index a8d049f42..7f3dd87de 100644 --- a/js/core/src/yas/nativeDesktopMedia.ts +++ b/js/core/src/yas/nativeDesktopMedia.ts @@ -15,8 +15,8 @@ import { type DesktopRevision, type NativeDesktopController, type TrayItem, -} from "../desktopModel"; -import { AudioPlayer } from "../AudioPlayer"; +} from "../desktopModel.js"; +import { AudioPlayer } from "../AudioPlayer.js"; import { ACTIVE_CAMERA, ACTIVE_MICROPHONE, @@ -50,26 +50,26 @@ import { type NativeMprisController, type PortalRequest, type PortalChoiceValue, -} from "../mediaModel"; +} from "../mediaModel.js"; import type { YasClientAuxSubscription, YasClientInfo, YasClientList, YasClientOrigin as ProductClientOrigin, -} from "../types"; -import * as g from "./generated"; +} from "../types.js"; +import * as g from "./generated.js"; import { YasClientClient, type YasClientOrigin, type YasClientRecord, type YasClientSnapshot, -} from "./client"; +} from "./client.js"; import { YasDesktopClient, type YasDesktopNotificationRecord, type YasDesktopSnapshot, type YasDesktopTrayRecord, -} from "./desktop"; +} from "./desktop.js"; import { YasMediaClient, mediaPlayerActive, @@ -82,9 +82,14 @@ import { type YasMediaPortalRequest, type YasMediaSnapshot, type YasMediaStreamStatus, -} from "./media"; -import type { YasConnection } from "./session"; -import { YasCursor, YasProtocolError, YasResultError, YasWriter } from "./wire"; +} from "./media.js"; +import type { YasConnection } from "./session.js"; +import { + YasCursor, + YasProtocolError, + YasResultError, + YasWriter, +} from "./wire.js"; const EMPTY_IMAGE: DesktopImage = { width: 0, diff --git a/js/core/src/yas/nativeExtensionFacade.ts b/js/core/src/yas/nativeExtensionFacade.ts index a6af77be2..3801ac4b9 100644 --- a/js/core/src/yas/nativeExtensionFacade.ts +++ b/js/core/src/yas/nativeExtensionFacade.ts @@ -4,7 +4,7 @@ * flags, and 32-byte content hash. */ -import * as g from "./generated"; +import * as g from "./generated.js"; import { type YasExtensionDefinitionIdentity, type YasExtensionRecord, @@ -12,9 +12,9 @@ import { type YasExtensionSnapshot, YasExtensionClient, extensionLimitsFromExtensions, -} from "./extension"; -import type { YasConnection } from "./session"; -import { YasDisconnectedError, YasProtocolError } from "./wire"; +} from "./extension.js"; +import type { YasConnection } from "./session.js"; +import { YasDisconnectedError, YasProtocolError } from "./wire.js"; const encoder = new TextEncoder(); diff --git a/js/core/src/yas/nativeProductFamilies.ts b/js/core/src/yas/nativeProductFamilies.ts index 630bbfe51..cc7b8dec5 100644 --- a/js/core/src/yas/nativeProductFamilies.ts +++ b/js/core/src/yas/nativeProductFamilies.ts @@ -28,19 +28,19 @@ import { YAS_CHANNEL_VERSION, YAS_STATUS_UNAVAILABLE, YAS_STATUS_UNSUPPORTED, -} from "./generated"; -import { YasChannelClient } from "./channel"; -import { YasEnvClient } from "./env"; -import { YasEventsClient } from "./events"; -import { YasExtensionClient } from "./extension"; -import { YasFsClient } from "./fs"; -import { YasGitClient } from "./git"; -import { YasKvClient } from "./kv"; -import { YasLspClient } from "./lsp"; -import { YasNetClient } from "./net"; -import { YasProcessClient } from "./process"; -import type { YasConnection } from "./session"; -import { YasProtocolError, YasResultError } from "./wire"; +} from "./generated.js"; +import { YasChannelClient } from "./channel.js"; +import { YasEnvClient } from "./env.js"; +import { YasEventsClient } from "./events.js"; +import { YasExtensionClient } from "./extension.js"; +import { YasFsClient } from "./fs.js"; +import { YasGitClient } from "./git.js"; +import { YasKvClient } from "./kv.js"; +import { YasLspClient } from "./lsp.js"; +import { YasNetClient } from "./net.js"; +import { YasProcessClient } from "./process.js"; +import type { YasConnection } from "./session.js"; +import { YasProtocolError, YasResultError } from "./wire.js"; interface FamilyClientFactories { fs(connection: YasConnection): YasFsClient; diff --git a/js/core/src/yas/nativeRelayTransport.ts b/js/core/src/yas/nativeRelayTransport.ts index 7a914b975..1bf26afad 100644 --- a/js/core/src/yas/nativeRelayTransport.ts +++ b/js/core/src/yas/nativeRelayTransport.ts @@ -2,14 +2,14 @@ import type { ConnectionStatus, YasTransportEventMap, YasTransportMessage, -} from "../types"; +} from "../types.js"; import { YasRelayClient, type YasRelayLink, type YasRelayRoute, type YasRelayTunnelTransport, -} from "./relay"; -import type { YasTransport } from "./session"; +} from "./relay.js"; +import type { YasTransport } from "./session.js"; /** Reconnectable raw YAS tunnel for one Relay route. * diff --git a/js/core/src/yas/nativeWorkspaceFs.ts b/js/core/src/yas/nativeWorkspaceFs.ts index a79a54274..24fa04954 100644 --- a/js/core/src/yas/nativeWorkspaceFs.ts +++ b/js/core/src/yas/nativeWorkspaceFs.ts @@ -15,10 +15,10 @@ import { type FsGrepOptions, type FsGrepResult, type FsSyncOptions, -} from "../fsModel"; -import { Notifier } from "../reactive"; -import type { SessionId } from "../types"; -import * as g from "./generated"; +} from "../fsModel.js"; +import { Notifier } from "../reactive.js"; +import type { SessionId } from "../types.js"; +import * as g from "./generated.js"; import { YasFsClient, decodeFsConflictDetail, @@ -35,8 +35,8 @@ import { type YasFsQueryRecord, type YasFsRoot, type YasFsRootSource, -} from "./fs"; -import type { YasConnection } from "./session"; +} from "./fs.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -48,8 +48,8 @@ import { YAS_STATE_SNAPSHOT_END, YAS_STATE_SNAPSHOT_RECORDS, type YasStateBatch, -} from "./state"; -import { YasProtocolError, YasResultError } from "./wire"; +} from "./state.js"; +import { YasProtocolError, YasResultError } from "./wire.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8", { fatal: true }); diff --git a/js/core/src/yas/nativeWorkspaceGit.ts b/js/core/src/yas/nativeWorkspaceGit.ts index 0c79f2509..3eda7fdac 100644 --- a/js/core/src/yas/nativeWorkspaceGit.ts +++ b/js/core/src/yas/nativeWorkspaceGit.ts @@ -1,10 +1,10 @@ /** Workspace-facing Git operations backed directly by typed YAS Git. */ -import * as model from "../gitModel"; -import { Notifier } from "../reactive"; -import type { SessionId } from "../types"; -import * as g from "./generated"; -import { decodeFsPath, type YasFsPath } from "./fs"; +import * as model from "../gitModel.js"; +import { Notifier } from "../reactive.js"; +import type { SessionId } from "../types.js"; +import * as g from "./generated.js"; +import { decodeFsPath, type YasFsPath } from "./fs.js"; import { YasGitClient, type YasGitContentRecord, @@ -17,9 +17,9 @@ import { type YasGitRepositorySource, type YasGitSnapshot, type YasGitWatchedQuery, -} from "./git"; -import type { YasConnection } from "./session"; -import { YasProtocolError, YasResultError } from "./wire"; +} from "./git.js"; +import type { YasConnection } from "./session.js"; +import { YasProtocolError, YasResultError } from "./wire.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8", { ignoreBOM: true }); @@ -772,7 +772,7 @@ class NativeGitLogSubscription implements YasNativeGitLogSubscription { } private async deliver( - update: import("./git").YasGitWatchedQueryUpdate, + update: import("./git.js").YasGitWatchedQueryUpdate, ): Promise { if (this.closed) return; if (!update.page) { diff --git a/js/core/src/yas/nativeWorkspaceKv.ts b/js/core/src/yas/nativeWorkspaceKv.ts index 447641443..0c3e9ac7d 100644 --- a/js/core/src/yas/nativeWorkspaceKv.ts +++ b/js/core/src/yas/nativeWorkspaceKv.ts @@ -4,7 +4,7 @@ import { YAS_FAMILY_KV, YAS_KV_MAX_INLINE_BYTES, YAS_KV_MAX_KEY_BYTES, -} from "./generated"; +} from "./generated.js"; import { YasKvClient, YasKvConflictError, @@ -12,16 +12,16 @@ import { type YasKvPrecondition, type YasKvStateChange, type YasKvStateUpdate, -} from "./kv"; -import type { YasConnection } from "./session"; +} from "./kv.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_DELTA, YAS_STATE_RESET, YAS_STATE_SNAPSHOT_BEGIN, YAS_STATE_SNAPSHOT_END, YAS_STATE_SNAPSHOT_RECORDS, -} from "./state"; -import { YasProtocolError } from "./wire"; +} from "./state.js"; +import { YasProtocolError } from "./wire.js"; import { WorkspaceSessionKvConflictError, copyWorkspaceSessionHash, @@ -32,7 +32,7 @@ import { type WorkspaceSessionKvWatch, type WorkspaceSessionKvWatchOptions, type WorkspaceSessionOwnedKv, -} from "../workspaceSessionKv"; +} from "../workspaceSessionKv.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8", { fatal: true }); diff --git a/js/core/src/yas/nativeWorkspaceLsp.ts b/js/core/src/yas/nativeWorkspaceLsp.ts index 3b81ce7c1..e5fb05e4b 100644 --- a/js/core/src/yas/nativeWorkspaceLsp.ts +++ b/js/core/src/yas/nativeWorkspaceLsp.ts @@ -15,11 +15,11 @@ import { LSP_STATUS_WARMING, LSP_STATUS_WRONG_TYPE, type LspOpenOptions, -} from "../lspModel"; -import { Notifier } from "../reactive"; -import type { SessionId } from "../types"; -import * as g from "./generated"; -import type { YasFsPath } from "./fs"; +} from "../lspModel.js"; +import { Notifier } from "../reactive.js"; +import type { SessionId } from "../types.js"; +import * as g from "./generated.js"; +import type { YasFsPath } from "./fs.js"; import { YasLspClient, decodeLspNoBackendDetail, @@ -29,9 +29,9 @@ import { type YasLspQueryRecord, type YasLspSnapshot, type YasLspWorkspace, -} from "./lsp"; -import type { YasConnection } from "./session"; -import { YasProtocolError } from "./wire"; +} from "./lsp.js"; +import type { YasConnection } from "./session.js"; +import { YasProtocolError } from "./wire.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8", { fatal: true }); diff --git a/js/core/src/yas/net.ts b/js/core/src/yas/net.ts index 60fee7bd2..4b7a33ad4 100644 --- a/js/core/src/yas/net.ts +++ b/js/core/src/yas/net.ts @@ -1,7 +1,7 @@ /** YAS Net family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection, YasReceiveBudgetLease } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection, YasReceiveBudgetLease } from "./session.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_MODE_MESSAGE, @@ -10,7 +10,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasDisconnectedError, @@ -20,7 +20,7 @@ import { decodeExtensions, encodeExtensions, type YasExtension, -} from "./wire"; +} from "./wire.js"; export { YAS_FAMILY_NET, @@ -29,7 +29,7 @@ export { YAS_NET_DATAGRAM_STATS, YAS_NET_OPEN, YAS_NET_VERSION, -} from "./generated"; +} from "./generated.js"; export type YasNetAddress = | { kind: "tcp"; host: string; port: number } diff --git a/js/core/src/yas/packed.ts b/js/core/src/yas/packed.ts index 4c4804488..65cbe23ed 100644 --- a/js/core/src/yas/packed.ts +++ b/js/core/src/yas/packed.ts @@ -1,12 +1,12 @@ /** Validators for TOML-generated packed codecs. */ -import * as g from "./generated"; -import { decodeEventsBatch, encodeEventsBatch } from "./events"; +import * as g from "./generated.js"; +import { decodeEventsBatch, encodeEventsBatch } from "./events.js"; import { decodeTerminalGridV1, type YasTerminalGridState, -} from "./terminal-grid"; -import { YasCursor, YasProtocolError, YasWriter } from "./wire"; +} from "./terminal-grid.js"; +import { YasCursor, YasProtocolError, YasWriter } from "./wire.js"; export interface YasSurfaceColorSpace { primaries: number; diff --git a/js/core/src/yas/process.ts b/js/core/src/yas/process.ts index 0b24706a1..d6d7afd7c 100644 --- a/js/core/src/yas/process.ts +++ b/js/core/src/yas/process.ts @@ -1,7 +1,7 @@ /** YAS Process family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection, YasReceiveBudgetLease } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection, YasReceiveBudgetLease } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -18,7 +18,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_RECEIVER_TO_SENDER, @@ -28,7 +28,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -37,7 +37,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_FAMILY_PROCESS, @@ -50,7 +50,7 @@ export { YAS_PROCESS_VERSION, YAS_PROCESS_WAIT, YAS_PROCESS_WATCH, -} from "./generated"; +} from "./generated.js"; export type YasProcessCwd = | { kind: "server-default" } diff --git a/js/core/src/yas/recording.ts b/js/core/src/yas/recording.ts index 1f8a160b3..bb5574af9 100644 --- a/js/core/src/yas/recording.ts +++ b/js/core/src/yas/recording.ts @@ -2,9 +2,9 @@ import { YAS_HARD_MAX_WIRE_FRAME, YAS_TERMINAL_FRAME_KEYFRAME, YAS_TERMINAL_GRID_CODEC_V1, -} from "./generated"; -import { decodeTerminalFrame, type YasTerminalFrameEvent } from "./terminal"; -import { YasCursor, YasProtocolError } from "./wire"; +} from "./generated.js"; +import { decodeTerminalFrame, type YasTerminalFrameEvent } from "./terminal.js"; +import { YasCursor, YasProtocolError } from "./wire.js"; const YASREC1_MAGIC = new Uint8Array([ 0x59, 0x41, 0x53, 0x52, 0x45, 0x43, 0x31, 0x0a, diff --git a/js/core/src/yas/relay.ts b/js/core/src/yas/relay.ts index 463b1c3ff..255c698dc 100644 --- a/js/core/src/yas/relay.ts +++ b/js/core/src/yas/relay.ts @@ -2,8 +2,8 @@ import type { YasTransportEventMap, YasTransportMessage, ConnectionStatus, -} from "../types"; -import { YAS_FAMILY_RELAY } from "./core"; +} from "../types.js"; +import { YAS_FAMILY_RELAY } from "./core.js"; import { YAS_RELAY_AVAILABILITY_AVAILABLE, YAS_RELAY_AVAILABILITY_DEGRADED, @@ -38,12 +38,12 @@ import { YAS_RELAY_UNWATCH, YAS_RELAY_VERSION, YAS_RELAY_WATCH, -} from "./generated"; +} from "./generated.js"; import { YasConnection, type YasConnectionOptions, type YasTransport, -} from "./session"; +} from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -59,7 +59,7 @@ import { estimateStateRetainedBytes, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_RECEIVER_TO_SENDER, @@ -67,7 +67,7 @@ import { decodeTransferDescriptor, transfersFor, type YasTransfer, -} from "./transfer"; +} from "./transfer.js"; import { YAS_STATUS_CANCELLED, YAS_STATUS_RESOURCE_EXHAUSTED, @@ -78,7 +78,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_RELAY_AVAILABILITY_AVAILABLE, @@ -100,7 +100,7 @@ export { YAS_RELAY_UNWATCH, YAS_RELAY_VERSION, YAS_RELAY_WATCH, -} from "./generated"; +} from "./generated.js"; export interface YasRelayRoute { handle: bigint; diff --git a/js/core/src/yas/selection.ts b/js/core/src/yas/selection.ts index 5566942e4..f375d1c64 100644 --- a/js/core/src/yas/selection.ts +++ b/js/core/src/yas/selection.ts @@ -40,8 +40,8 @@ import { YAS_SELECTION_WATCH, YAS_STATUS_NOT_FOUND, YAS_TRANSFER_SENSITIVE_CONTENT_EXTENSION, -} from "./generated"; -import type { YasConnection } from "./session"; +} from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -58,7 +58,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_RECEIVER_TO_SENDER, @@ -70,7 +70,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -81,7 +81,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_FAMILY_SELECTION, @@ -119,7 +119,7 @@ export { YAS_SELECTION_UNWATCH, YAS_SELECTION_VERSION, YAS_SELECTION_WATCH, -} from "./generated"; +} from "./generated.js"; export interface YasSelectionInlineItem { mime: string; diff --git a/js/core/src/yas/session.ts b/js/core/src/yas/session.ts index d832669d0..87955ef2f 100644 --- a/js/core/src/yas/session.ts +++ b/js/core/src/yas/session.ts @@ -2,7 +2,7 @@ import type { YasTransport as BaseYasTransport, YasTransportMessage, ConnectionStatus, -} from "../types"; +} from "../types.js"; import { YAS_CORE_CANCEL, YAS_CORE_FAMILY_UPDATE, @@ -35,10 +35,10 @@ import { type YasFamilyDescriptor, type YasGoAway, type YasServerHello, -} from "./core"; -import type { YasExtension } from "./wire"; -import { YAS_FAMILY_DEPENDENCIES } from "./generated"; -import { validateYasDatagramFrame } from "./datagram"; +} from "./core.js"; +import type { YasExtension } from "./wire.js"; +import { YAS_FAMILY_DEPENDENCIES } from "./generated.js"; +import { validateYasDatagramFrame } from "./datagram.js"; import { YAS_CLASS_EVENT, YAS_CLASS_REQUEST, @@ -64,7 +64,7 @@ import { encodeYasFrame, frameForByteStream, type YasFrame, -} from "./wire"; +} from "./wire.js"; export interface YasTransport extends BaseYasTransport { /** Browser transports use messages; Relay tunnels expose a raw byte stream. */ diff --git a/js/core/src/yas/state.ts b/js/core/src/yas/state.ts index 7afa63588..1676e53db 100644 --- a/js/core/src/yas/state.ts +++ b/js/core/src/yas/state.ts @@ -1,5 +1,5 @@ -import type { YasConnection, YasReceiveBudgetLease } from "./session"; -import type { ConnectionStatus } from "../types"; +import type { YasConnection, YasReceiveBudgetLease } from "./session.js"; +import type { ConnectionStatus } from "../types.js"; import { YAS_STATE_MODE_REPLAY, YAS_STATE_MODE_SNAPSHOT, @@ -13,7 +13,7 @@ import { YAS_STATE_RECORD_REMOVE, YAS_STATE_RECORD_REPLACE, YAS_STATE_WATCH_RESUME, -} from "./generated"; +} from "./generated.js"; import { YasCursor, YasProtocolError, @@ -23,7 +23,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export const YAS_STATE_ADD = YAS_STATE_RECORD_ADD; export const YAS_STATE_REPLACE = YAS_STATE_RECORD_REPLACE; diff --git a/js/core/src/yas/surface.ts b/js/core/src/yas/surface.ts index 152e72a02..3d92a484a 100644 --- a/js/core/src/yas/surface.ts +++ b/js/core/src/yas/surface.ts @@ -6,7 +6,7 @@ import { YAS_SURFACE_COLOR_CAP_HDR10_AV1_444, YAS_SURFACE_COLOR_CAP_AV1_444, YAS_SURFACE_COLOR_CAP_H264_444, -} from "./generated"; +} from "./generated.js"; /** YAS Surface family v1 codecs and browser client. */ import { @@ -80,8 +80,8 @@ import { YAS_SURFACE_TEXT_INPUT_REQUESTED, YAS_SURFACE_TOUCH_PHASE_CANCEL, YAS_SURFACE_TOUCH_PHASE_FRAME, -} from "./generated"; -import type { YasConnection, YasReceiveBudgetLease } from "./session"; +} from "./generated.js"; +import type { YasConnection, YasReceiveBudgetLease } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -99,7 +99,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_SENDER_TO_RECEIVER, @@ -107,7 +107,7 @@ import { transfersFor, type YasInlineOrTransfer, type YasTransfer, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -116,7 +116,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_FAMILY_SURFACE, @@ -144,7 +144,7 @@ export { YAS_SURFACE_UNWATCH, YAS_SURFACE_VERSION, YAS_SURFACE_WATCH, -} from "./generated"; +} from "./generated.js"; export interface YasSurfaceRecord { surfaceHandle: bigint; diff --git a/js/core/src/yas/terminal-grid.ts b/js/core/src/yas/terminal-grid.ts index 3371c63c3..014b8ced2 100644 --- a/js/core/src/yas/terminal-grid.ts +++ b/js/core/src/yas/terminal-grid.ts @@ -1,4 +1,4 @@ -import { lz4Decompress } from "../lz4"; +import { lz4Decompress } from "../lz4.js"; import { YAS_TERMINAL_FRAME_CODEC_COMPRESSED, YAS_TERMINAL_FRAME_COMPONENTS, @@ -14,9 +14,9 @@ import { YAS_TERMINAL_GRID_CODEC_V1, YAS_TERMINAL_CELL_BYTES, YAS_TERMINAL_COMPONENT_KEYBOARD_FLAGS, -} from "./generated"; -import type { YasTerminalFrameEvent } from "./terminal"; -import { YasCursor, YasProtocolError } from "./wire"; +} from "./generated.js"; +import type { YasTerminalFrameEvent } from "./terminal.js"; +import { YasCursor, YasProtocolError } from "./wire.js"; const FRAME_KNOWN_FLAGS = YAS_TERMINAL_FRAME_KEYFRAME | diff --git a/js/core/src/yas/terminal.ts b/js/core/src/yas/terminal.ts index 530a178b2..63bcbc4df 100644 --- a/js/core/src/yas/terminal.ts +++ b/js/core/src/yas/terminal.ts @@ -1,4 +1,4 @@ -import * as g from "./generated"; +import * as g from "./generated.js"; import { YAS_CLASS_EVENT, YAS_FAMILY_TERMINAL, @@ -90,8 +90,8 @@ import { YAS_TERMINAL_WHEEL_AT, YAS_TERMINAL_WHEEL_SOURCE_CONTINUOUS, YAS_TERMINAL_WRITE, -} from "./generated"; -import type { YasConnection, YasReceiveBudgetLease } from "./session"; +} from "./generated.js"; +import type { YasConnection, YasReceiveBudgetLease } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -109,7 +109,7 @@ import { estimateStateRetainedBytes, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_SENDER_TO_RECEIVER, @@ -119,7 +119,7 @@ import { type YasTransfer, type YasTransferDescriptor, type YasTransferManager, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YAS_MAX_BULK_CHUNK, @@ -129,9 +129,9 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; -export * from "./terminal-grid"; +export * from "./terminal-grid.js"; export { YAS_FAMILY_TERMINAL, YAS_TERMINAL_CLOSE, @@ -166,7 +166,7 @@ export { YAS_TERMINAL_WATCH, YAS_TERMINAL_WHEEL, YAS_TERMINAL_WRITE, -} from "./generated"; +} from "./generated.js"; const terminalStateTags = new Set([ YAS_TERMINAL_STATE_TITLE_EXTENSION, diff --git a/js/core/src/yas/terminalRenderer.ts b/js/core/src/yas/terminalRenderer.ts index a68961f49..ec96cb284 100644 --- a/js/core/src/yas/terminalRenderer.ts +++ b/js/core/src/yas/terminalRenderer.ts @@ -1,6 +1,6 @@ -import { lz4Compress } from "../lz4"; -import type { YasTerminalGridState } from "./terminal"; -import { YasWriter } from "./wire"; +import { lz4Compress } from "../lz4.js"; +import type { YasTerminalGridState } from "./terminal.js"; +import { YasWriter } from "./wire.js"; // Browser renderer ingest codec. This is an internal WASM boundary, not a YAS // transport frame: native handles and family frames never enter this byte diff --git a/js/core/src/yas/transfer.ts b/js/core/src/yas/transfer.ts index 617465047..6629c816f 100644 --- a/js/core/src/yas/transfer.ts +++ b/js/core/src/yas/transfer.ts @@ -1,5 +1,5 @@ -import { YAS_FAMILY_TRANSFER } from "./core"; -import type { ConnectionStatus } from "../types"; +import { YAS_FAMILY_TRANSFER } from "./core.js"; +import type { ConnectionStatus } from "../types.js"; import { YAS_TRANSFER_BYTE_DATA, YAS_TRANSFER_CLOSE, @@ -18,8 +18,8 @@ import { YAS_TRANSFER_SENSITIVE_CONTENT_EXTENSION, YAS_TRANSFER_UPLOAD_STAGE_EXTENSION, YAS_TRANSFER_VERSION, -} from "./generated"; -import type { YasConnection, YasReceiveBudgetLease } from "./session"; +} from "./generated.js"; +import type { YasConnection, YasReceiveBudgetLease } from "./session.js"; import { YAS_MAX_BULK_CHUNK, YAS_STATUS_CANCELLED, @@ -32,7 +32,7 @@ import { decodeExtensions, encodeExtensions, type YasExtension, -} from "./wire"; +} from "./wire.js"; export { YAS_TRANSFER_BYTE_DATA, @@ -43,7 +43,7 @@ export { YAS_TRANSFER_MODE_MESSAGE, YAS_TRANSFER_RESET, YAS_TRANSFER_VERSION, -} from "./generated"; +} from "./generated.js"; export const YAS_TRANSFER_RECEIVER_TO_SENDER = YAS_TRANSFER_DIRECTION_RECEIVER_TO_SENDER; diff --git a/js/core/src/yas/wire.ts b/js/core/src/yas/wire.ts index b5c3081db..ba2ca7bf8 100644 --- a/js/core/src/yas/wire.ts +++ b/js/core/src/yas/wire.ts @@ -18,7 +18,7 @@ import { YAS_PRE_HELLO_MAX_FRAME, YAS_STATUS_OK, YAS_STREAM_LENGTH_BYTES, -} from "./generated"; +} from "./generated.js"; export { YAS_CLASS_EVENT, @@ -40,7 +40,7 @@ export { YAS_STATUS_STALE, YAS_STATUS_IO, YAS_STATUS_INTERNAL, -} from "./generated"; +} from "./generated.js"; export const YAS_PREFACE = Uint8Array.from( YAS_PREFACE_HEX.match(/../g)!.map((byte) => Number.parseInt(byte, 16)), diff --git a/js/core/tsconfig.json b/js/core/tsconfig.json index cc34ddfb5..1e95c4018 100644 --- a/js/core/tsconfig.json +++ b/js/core/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", + "module": "NodeNext", + "moduleResolution": "NodeNext", "paths": { "@yas-run/browser": ["../../crates/browser/pkg/yas_browser.d.ts"] }, diff --git a/js/react/src/YasSurfaceView.tsx b/js/react/src/YasSurfaceView.tsx index 9709aee2c..01e7be18a 100644 --- a/js/react/src/YasSurfaceView.tsx +++ b/js/react/src/YasSurfaceView.tsx @@ -16,7 +16,7 @@ import type { SurfaceId, SurfaceTouchMode, } from "@yas-run/core"; -import { useRequiredYasWorkspace } from "./YasContext"; +import { useRequiredYasWorkspace } from "./YasContext.js"; export interface YasSurfaceViewProps { connectionId: ConnectionId; diff --git a/js/react/src/YasTerminal.tsx b/js/react/src/YasTerminal.tsx index 9a3ce7c99..894cf58de 100644 --- a/js/react/src/YasTerminal.tsx +++ b/js/react/src/YasTerminal.tsx @@ -3,10 +3,10 @@ import type { Ref } from "react"; import type { ConnectionStatus } from "@yas-run/core"; import type { Terminal } from "@yas-run/browser"; import { YasTerminalSurface } from "@yas-run/core"; -import type { YasTerminalProps } from "./types"; -import { useYasContext, useRequiredYasWorkspace } from "./YasContext"; -import { useYasConnection } from "./hooks/useYasConnection"; -import { useYasSession } from "./hooks/useYasSession"; +import type { YasTerminalProps } from "./types.js"; +import { useYasContext, useRequiredYasWorkspace } from "./YasContext.js"; +import { useYasConnection } from "./hooks/useYasConnection.js"; +import { useYasSession } from "./hooks/useYasSession.js"; // --------------------------------------------------------------------------- // Public handle exposed via ref diff --git a/js/react/src/hooks/index.ts b/js/react/src/hooks/index.ts index 9f5a0f921..b99e3227d 100644 --- a/js/react/src/hooks/index.ts +++ b/js/react/src/hooks/index.ts @@ -1,5 +1,5 @@ -export { useYasConnection } from "./useYasConnection"; -export { useYasSessions } from "./useYasSessions"; -export { useYasWorkspace, useYasWorkspaceState } from "./useYasWorkspace"; -export { useYasSession, useYasFocusedSession } from "./useYasSession"; -export { useYasWorkspaceConnection } from "./useYasWorkspaceConnection"; +export { useYasConnection } from "./useYasConnection.js"; +export { useYasSessions } from "./useYasSessions.js"; +export { useYasWorkspace, useYasWorkspaceState } from "./useYasWorkspace.js"; +export { useYasSession, useYasFocusedSession } from "./useYasSession.js"; +export { useYasWorkspaceConnection } from "./useYasWorkspaceConnection.js"; diff --git a/js/react/src/hooks/useYasConnection.ts b/js/react/src/hooks/useYasConnection.ts index 640384ca5..7d74bd248 100644 --- a/js/react/src/hooks/useYasConnection.ts +++ b/js/react/src/hooks/useYasConnection.ts @@ -1,6 +1,6 @@ import { useSyncExternalStore } from "react"; import type { YasConnectionSnapshot, ConnectionId } from "@yas-run/core"; -import { useRequiredYasWorkspace } from "../YasContext"; +import { useRequiredYasWorkspace } from "../YasContext.js"; export function useYasConnection( connectionId?: ConnectionId, diff --git a/js/react/src/hooks/useYasSession.ts b/js/react/src/hooks/useYasSession.ts index 602bc13d5..f84e36c3c 100644 --- a/js/react/src/hooks/useYasSession.ts +++ b/js/react/src/hooks/useYasSession.ts @@ -1,6 +1,6 @@ import { useSyncExternalStore } from "react"; import type { YasSession, SessionId } from "@yas-run/core"; -import { useRequiredYasWorkspace } from "../YasContext"; +import { useRequiredYasWorkspace } from "../YasContext.js"; export function useYasSession(sessionId: SessionId | null): YasSession | null { const workspace = useRequiredYasWorkspace(); diff --git a/js/react/src/hooks/useYasSessions.ts b/js/react/src/hooks/useYasSessions.ts index 07487c24e..884cd2dc5 100644 --- a/js/react/src/hooks/useYasSessions.ts +++ b/js/react/src/hooks/useYasSessions.ts @@ -1,6 +1,6 @@ import { useSyncExternalStore } from "react"; import type { YasSession } from "@yas-run/core"; -import { useRequiredYasWorkspace } from "../YasContext"; +import { useRequiredYasWorkspace } from "../YasContext.js"; export function useYasSessions(): readonly YasSession[] { const workspace = useRequiredYasWorkspace(); diff --git a/js/react/src/hooks/useYasWorkspace.ts b/js/react/src/hooks/useYasWorkspace.ts index bfc05b25a..bcda499be 100644 --- a/js/react/src/hooks/useYasWorkspace.ts +++ b/js/react/src/hooks/useYasWorkspace.ts @@ -1,6 +1,6 @@ import { useSyncExternalStore } from "react"; import type { YasWorkspaceSnapshot } from "@yas-run/core"; -import { useRequiredYasWorkspace } from "../YasContext"; +import { useRequiredYasWorkspace } from "../YasContext.js"; export function useYasWorkspace() { return useRequiredYasWorkspace(); diff --git a/js/react/src/index.ts b/js/react/src/index.ts index 2a5e79802..c16cca515 100644 --- a/js/react/src/index.ts +++ b/js/react/src/index.ts @@ -1,18 +1,21 @@ -export { YasTerminal } from "./YasTerminal"; -export type { YasTerminalHandle } from "./YasTerminal"; -export { YasSurfaceView } from "./YasSurfaceView"; +export { YasTerminal } from "./YasTerminal.js"; +export type { YasTerminalHandle } from "./YasTerminal.js"; +export { YasSurfaceView } from "./YasSurfaceView.js"; export type { YasSurfaceViewProps, YasSurfaceViewHandle, -} from "./YasSurfaceView"; +} from "./YasSurfaceView.js"; -export type { YasTerminalProps } from "./types"; +export type { YasTerminalProps } from "./types.js"; -export { useYasConnection } from "./hooks/useYasConnection"; -export { useYasSessions } from "./hooks/useYasSessions"; -export { useYasWorkspace, useYasWorkspaceState } from "./hooks/useYasWorkspace"; -export { useYasSession, useYasFocusedSession } from "./hooks/useYasSession"; -export { useYasWorkspaceConnection } from "./hooks/useYasWorkspaceConnection"; +export { useYasConnection } from "./hooks/useYasConnection.js"; +export { useYasSessions } from "./hooks/useYasSessions.js"; +export { + useYasWorkspace, + useYasWorkspaceState, +} from "./hooks/useYasWorkspace.js"; +export { useYasSession, useYasFocusedSession } from "./hooks/useYasSession.js"; +export { useYasWorkspaceConnection } from "./hooks/useYasWorkspaceConnection.js"; -export { YasWorkspaceProvider } from "./YasContext"; -export type { YasContextValue, YasProviderProps } from "./YasContext"; +export { YasWorkspaceProvider } from "./YasContext.js"; +export type { YasContextValue, YasProviderProps } from "./YasContext.js"; diff --git a/js/react/tsconfig.json b/js/react/tsconfig.json index f2716ac06..7c28e1e40 100644 --- a/js/react/tsconfig.json +++ b/js/react/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", + "module": "NodeNext", + "moduleResolution": "NodeNext", "jsx": "react-jsx", "paths": { "@yas-run/browser": ["../../crates/browser/pkg/yas_browser.d.ts"] diff --git a/js/solid/src/YasSurfaceView.tsx b/js/solid/src/YasSurfaceView.tsx index fa4bb451f..fd97fc245 100644 --- a/js/solid/src/YasSurfaceView.tsx +++ b/js/solid/src/YasSurfaceView.tsx @@ -15,7 +15,7 @@ import { driveSurfaceResize, } from "@yas-run/core"; import type { ConnectionId, SurfaceId, SurfaceTouchMode } from "@yas-run/core"; -import { useRequiredYasWorkspace } from "./YasContext"; +import { useRequiredYasWorkspace } from "./YasContext.js"; export interface YasSurfaceViewProps { connectionId: ConnectionId; diff --git a/js/solid/src/YasTerminal.tsx b/js/solid/src/YasTerminal.tsx index 7383a90c9..73886ef17 100644 --- a/js/solid/src/YasTerminal.tsx +++ b/js/solid/src/YasTerminal.tsx @@ -8,8 +8,8 @@ import { } from "solid-js"; import { YasTerminalSurface } from "@yas-run/core"; import type { SessionId, TerminalPalette } from "@yas-run/core"; -import { useYasContext, useRequiredYasWorkspace } from "./YasContext"; -import { createYasWorkspaceState } from "./hooks/createYasWorkspace"; +import { useYasContext, useRequiredYasWorkspace } from "./YasContext.js"; +import { createYasWorkspaceState } from "./hooks/createYasWorkspace.js"; export interface YasTerminalProps { sessionId: SessionId | null; diff --git a/js/solid/src/hooks/createYasSessions.ts b/js/solid/src/hooks/createYasSessions.ts index 5d5aa76b2..c2fecc822 100644 --- a/js/solid/src/hooks/createYasSessions.ts +++ b/js/solid/src/hooks/createYasSessions.ts @@ -1,5 +1,5 @@ import type { YasSession, YasWorkspace } from "@yas-run/core"; -import { createYasWorkspaceState } from "./createYasWorkspace"; +import { createYasWorkspaceState } from "./createYasWorkspace.js"; export function createYasSessions( workspace?: YasWorkspace, diff --git a/js/solid/src/hooks/createYasWorkspace.ts b/js/solid/src/hooks/createYasWorkspace.ts index e797d6ded..774421f13 100644 --- a/js/solid/src/hooks/createYasWorkspace.ts +++ b/js/solid/src/hooks/createYasWorkspace.ts @@ -1,6 +1,6 @@ import { createSignal, onCleanup } from "solid-js"; import type { YasWorkspace, YasWorkspaceSnapshot } from "@yas-run/core"; -import { useRequiredYasWorkspace } from "../YasContext"; +import { useRequiredYasWorkspace } from "../YasContext.js"; export function createYasWorkspace(): YasWorkspace { return useRequiredYasWorkspace(); diff --git a/js/solid/src/hooks/index.ts b/js/solid/src/hooks/index.ts index 755c5e627..2256ecf36 100644 --- a/js/solid/src/hooks/index.ts +++ b/js/solid/src/hooks/index.ts @@ -1,8 +1,8 @@ -export { useYasConnection } from "./useYasConnection"; -export { createYasSessions } from "./createYasSessions"; +export { useYasConnection } from "./useYasConnection.js"; +export { createYasSessions } from "./createYasSessions.js"; export { createYasWorkspace, createYasWorkspaceState, -} from "./createYasWorkspace"; -export { useYasSession, useYasFocusedSession } from "./useYasSession"; -export { createYasWorkspaceConnection } from "./createYasWorkspaceConnection"; +} from "./createYasWorkspace.js"; +export { useYasSession, useYasFocusedSession } from "./useYasSession.js"; +export { createYasWorkspaceConnection } from "./createYasWorkspaceConnection.js"; diff --git a/js/solid/src/index.ts b/js/solid/src/index.ts index 3d8745a17..0001b9232 100644 --- a/js/solid/src/index.ts +++ b/js/solid/src/index.ts @@ -1,17 +1,17 @@ -export { YasTerminal } from "./YasTerminal"; -export type { YasTerminalProps } from "./YasTerminal"; +export { YasTerminal } from "./YasTerminal.js"; +export type { YasTerminalProps } from "./YasTerminal.js"; -export { YasSurfaceView } from "./YasSurfaceView"; -export type { YasSurfaceViewProps } from "./YasSurfaceView"; +export { YasSurfaceView } from "./YasSurfaceView.js"; +export type { YasSurfaceViewProps } from "./YasSurfaceView.js"; -export { useYasConnection } from "./hooks/useYasConnection"; -export { createYasSessions } from "./hooks/createYasSessions"; +export { useYasConnection } from "./hooks/useYasConnection.js"; +export { createYasSessions } from "./hooks/createYasSessions.js"; export { createYasWorkspace, createYasWorkspaceState, -} from "./hooks/createYasWorkspace"; -export { useYasSession, useYasFocusedSession } from "./hooks/useYasSession"; -export { createYasWorkspaceConnection } from "./hooks/createYasWorkspaceConnection"; +} from "./hooks/createYasWorkspace.js"; +export { useYasSession, useYasFocusedSession } from "./hooks/useYasSession.js"; +export { createYasWorkspaceConnection } from "./hooks/createYasWorkspaceConnection.js"; -export { YasWorkspaceProvider } from "./YasContext"; -export type { YasContextValue, YasProviderProps } from "./YasContext"; +export { YasWorkspaceProvider } from "./YasContext.js"; +export type { YasContextValue, YasProviderProps } from "./YasContext.js"; diff --git a/js/solid/tsconfig.json b/js/solid/tsconfig.json index 230ac451a..03c1a1db0 100644 --- a/js/solid/tsconfig.json +++ b/js/solid/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", + "module": "NodeNext", + "moduleResolution": "NodeNext", "jsx": "preserve", "jsxImportSource": "solid-js", "paths": { From 394b643942574a9264f16d27ae31da8146ab6598 Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 19:32:13 +0000 Subject: [PATCH 05/19] Call the wasm init export through the loaded module The Nix build's local @yas-run/browser package has no "type": "module", so under NodeNext its default export types as the module namespace and tsc rejects init() as not callable. Using the default export from the dynamic import types the same way in both layouts. --- js/core/src/node-wasm.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/js/core/src/node-wasm.ts b/js/core/src/node-wasm.ts index dc1a1bf2f..0d3294f70 100644 --- a/js/core/src/node-wasm.ts +++ b/js/core/src/node-wasm.ts @@ -1,8 +1,6 @@ import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; -import init from "@yas-run/browser"; - import type { YasWasmModule } from "./TerminalStore.js"; /** @@ -30,7 +28,7 @@ import type { YasWasmModule } from "./TerminalStore.js"; */ export async function loadYasWasm(wasmPath?: string): Promise { const mod = (await import("@yas-run/browser")) as unknown as YasWasmModule & { - default?: unknown; + default?: (options: { module_or_path: unknown }) => Promise; }; // A self-initializing build (`--target nodejs`/`bundler`) has already @@ -45,8 +43,6 @@ export async function loadYasWasm(wasmPath?: string): Promise { ? fileURLToPath(location) : location; const bytes = await readFile(path); - await init({ - module_or_path: bytes as unknown as Parameters[0], - }); + await mod.default({ module_or_path: bytes }); return mod; } From 54aab99cf2f3a5b27d09249e74261891578ffc7b Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 18:03:26 +0000 Subject: [PATCH 06/19] Track the release in core's @yas-run/browser peer range @yas-run/core declared a peer on @yas-run/browser >=0.9.0, but the browser package is versioned with each YAS release (0.3.0 today), so a plain npm install failed with ERESOLVE. Use ^0.3.0 and have prepare-release stamp ^ alongside the package versions. The browser package is built from crates/browser outside the pnpm workspace, so workspace:* cannot do this at publish. Document the install and wait for the workspace to be ready before creating a session in the Node example; createSession throws until the Terminal family is negotiated. --- EMBEDDING.md | 18 +++++++++++++++++- bin/prepare-release | 9 +++++++++ js/core/package.json | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/EMBEDDING.md b/EMBEDDING.md index b8149f7d5..e9cff9be5 100644 --- a/EMBEDDING.md +++ b/EMBEDDING.md @@ -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"; @@ -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((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, diff --git a/bin/prepare-release b/bin/prepare-release index b50c67c29..944c3e41f 100755 --- a/bin/prepare-release +++ b/bin/prepare-release @@ -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 @@ -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 diff --git a/js/core/package.json b/js/core/package.json index b749b58ef..140c280b1 100644 --- a/js/core/package.json +++ b/js/core/package.json @@ -108,7 +108,7 @@ "test:watch": "vitest" }, "peerDependencies": { - "@yas-run/browser": ">=0.9.0" + "@yas-run/browser": "^0.3.0" }, "dependencies": { "tweetnacl": "^1.0.3" From 717160521c9108bc6bea58b2255b4c3a81150467 Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 18:03:26 +0000 Subject: [PATCH 07/19] Document direct as the touchMode default in React and Solid views YasSurfaceCanvas and both views default touchMode to "direct"; the view prop comments claimed pointer emulation. --- js/react/src/YasSurfaceView.tsx | 4 +++- js/solid/src/YasSurfaceView.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/js/react/src/YasSurfaceView.tsx b/js/react/src/YasSurfaceView.tsx index 01e7be18a..98aec5fd0 100644 --- a/js/react/src/YasSurfaceView.tsx +++ b/js/react/src/YasSurfaceView.tsx @@ -25,7 +25,9 @@ export interface YasSurfaceViewProps { style?: React.CSSProperties; /** Render cached frames without owning a server stream when false. */ live?: boolean; - /** How touchscreen contacts are delivered. Defaults to pointer emulation. */ + /** How touchscreen contacts are delivered. Defaults to `"direct"`, which + * forwards every contact to the Wayland client's `wl_touch`; `"pointer"` + * opts into single-finger click/scroll emulation. */ touchMode?: SurfaceTouchMode; /** * Whether this view owns its surface's size, resizing it to fill the diff --git a/js/solid/src/YasSurfaceView.tsx b/js/solid/src/YasSurfaceView.tsx index fd97fc245..52d430efb 100644 --- a/js/solid/src/YasSurfaceView.tsx +++ b/js/solid/src/YasSurfaceView.tsx @@ -43,7 +43,9 @@ export interface YasSurfaceViewProps { * do not create a server-side video subscription. Defaults to true. */ live?: boolean; - /** How touchscreen contacts are delivered. Defaults to pointer emulation. */ + /** How touchscreen contacts are delivered. Defaults to `"direct"`, which + * forwards every contact to the Wayland client's `wl_touch`; `"pointer"` + * opts into single-finger click/scroll emulation. */ touchMode?: SurfaceTouchMode; /** * Surface zoom factor, e.g. 1.25 for 125% or an exact 1.25x scale. From 7870c88cd543e0f2e560e1b3c51e650d1a9c6b06 Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 18:30:06 +0000 Subject: [PATCH 08/19] Resolve target names from the home server's remotes catalogue `yas remote add` writes the home server's `remotes` KV key, but `--on NAME`, YAS_TARGET and `yas.target` were looked up in the legacy yas.remotes file, so a freshly added remote was an unknown target and `yas remote set-default NAME` broke every command. Bare names now resolve against the home server (YAS_SOCK, else the default local instance), following name-to-name entries with cycle detection and refusing disabled entries. `yas remote` verbs without --on also edit the home server rather than the default target, so `toggle` after `set-default` reaches the catalogue that defines the name. Help text and docs describe the catalogue instead of the file. --- ARCHITECTURE.md | 17 ++-- SKILL.md | 2 +- crates/cli/src/cli.rs | 29 +++--- crates/cli/src/learn.md | 8 ++ crates/cli/src/main.rs | 6 ++ crates/cli/src/transport.rs | 164 ++++++++++++++++++++++++++++------ crates/cli/src/yas_remotes.rs | 18 +++- docs/design/net.md | 2 +- 8 files changed, 199 insertions(+), 47 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0962ea525..38a869e35 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 = ` — 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 @@ -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 @@ -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. diff --git a/SKILL.md b/SKILL.md index 06bc6de24..db0ce35cc 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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` diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 1e7ecf4e2..91a1e688b 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -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, @@ -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 @@ -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\ @@ -2660,8 +2666,8 @@ 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, @@ -2669,8 +2675,9 @@ pub enum RemoteCommand { /// 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. diff --git a/crates/cli/src/learn.md b/crates/cli/src/learn.md index 4d6b94802..f69e1a74d 100644 --- a/crates/cli/src/learn.md +++ b/crates/cli/src/learn.md @@ -226,6 +226,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 diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a085140af..53564c3cf 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -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?; diff --git a/crates/cli/src/transport.rs b/crates/cli/src/transport.rs index 1d89b1ed1..2f2bd6bfc 100644 --- a/crates/cli/src/transport.rs +++ b/crates/cli/src/transport.rs @@ -677,20 +677,64 @@ pub async fn connect_via_composite_proxy(upstream_uri: &str) -> Result Result { - Box::pin(connect_native_uri_inner( - uri, - hub, - std::collections::HashSet::new(), - )) - .await + if is_target_uri(uri) { + return Box::pin(connect_target_uri(uri, hub)).await; + } + let entries = crate::yas_remotes::read_home() + .await + .map_err(|error| format!("cannot look up remote '{uri}' on the home server: {error}"))?; + let resolved = resolve_remote_name(uri, &entries)?; + Box::pin(connect_target_uri(&resolved, hub)).await } -async fn connect_native_uri_inner( - uri: &str, - hub: &str, - mut visited: std::collections::HashSet, -) -> Result { +const TARGET_SCHEMES: &[&str] = &[ + "proxy:", "ssh:", "tcp:", "wt://", "ws://", "wss://", "uplink:", "socket:", "share:", "local:", +]; + +fn is_target_uri(uri: &str) -> bool { + uri == "local" || TARGET_SCHEMES.iter().any(|scheme| uri.starts_with(scheme)) +} + +/// Follow `name` through the catalogue to a URI. An entry may name another +/// entry; disabled entries do not resolve. +fn resolve_remote_name( + name: &str, + entries: &[yas_webserver::config::RemoteEntry], +) -> Result { + let mut visited = std::collections::HashSet::new(); + let mut current = name; + loop { + if !visited.insert(current) { + return Err(format!("remotes: cycle detected resolving '{name}'")); + } + let Some(entry) = entries.iter().rev().find(|entry| entry.name == current) else { + return Err(if current == name { + format!( + "unknown target '{name}' \ + (expected ssh:, tcp:, ws://, wss://, wt://, socket:, share:, uplink:, proxy:, \ + local[:NAME], or a remote name from `yas remote list` on the home server)" + ) + } else { + format!("remote '{name}' refers to '{current}', which is not a configured remote") + }); + }; + if entry.disabled { + return Err(format!( + "remote '{current}' is disabled; enable it with `yas remote toggle {current}`" + )); + } + if is_target_uri(&entry.uri) { + return Ok(entry.uri.clone()); + } + current = &entry.uri; + } +} + +async fn connect_target_uri(uri: &str, hub: &str) -> Result { if let Some(upstream) = uri.strip_prefix("proxy:") { return connect_via_native_proxy(upstream).await; } @@ -765,9 +809,7 @@ async fn connect_native_uri_inner( }); } if uri == "local" { - let path = yas_webserver::config::default_yas_socket(); - ensure_local_server(&path).await?; - return connect_native_home(&path).await; + return connect_home().await; } if let Some(raw_name) = uri.strip_prefix("local:") { let name: yas_server::ServerName = raw_name @@ -777,19 +819,7 @@ async fn connect_native_uri_inner( ensure_local_server_with_name(&path, Some(&name)).await?; return connect_native_home(&path).await; } - - let entries = yas_webserver::config::read_remotes(); - if let Some((_, target_uri)) = entries.into_iter().find(|(name, _)| name == uri) { - if !visited.insert(uri.to_string()) { - return Err(format!("yas.remotes: cycle detected resolving '{uri}'")); - } - return Box::pin(connect_native_uri_inner(&target_uri, hub, visited)).await; - } - Err(format!( - "unknown target '{uri}' \ - (expected ssh:, tcp:, ws://, wss://, wt://, socket:, share:, proxy:, local[:NAME], \ - or a name from yas.remotes)" - )) + Err(format!("unknown target '{uri}'")) } async fn connect_native_upstream(uri: &str) -> Result { @@ -879,6 +909,11 @@ pub async fn connect_native(on: &Option, hub: &str) -> Result Result { let path = yas_webserver::config::default_yas_socket(); ensure_local_server(&path).await?; connect_native_home(&path).await @@ -1303,4 +1338,79 @@ mod tests { let result = read_frame(&mut server).await.unwrap(); assert_eq!(result, payload); } + + fn remote(name: &str, uri: &str, disabled: bool) -> yas_webserver::config::RemoteEntry { + yas_webserver::config::RemoteEntry { + name: name.into(), + uri: uri.into(), + disabled, + } + } + + #[test] + fn target_uris_are_not_names() { + for uri in [ + "local", + "local:work", + "socket:/tmp/yas.sock", + "ssh:alice@host", + "tcp:127.0.0.1:1", + "ws://h", + "wss://h", + "wt://h", + "share:pass", + "uplink:https://relay#token", + "proxy:ssh:host", + ] { + assert!(is_target_uri(uri), "{uri}"); + } + for name in ["work", "localhost", "rabbit"] { + assert!(!is_target_uri(name), "{name}"); + } + } + + #[test] + fn remote_name_resolves_through_catalogue() { + let entries = [ + remote("work", "socket:/tmp/work.sock", false), + remote("alias", "work", false), + remote("secret", "share:passphrase", false), + ]; + assert_eq!( + resolve_remote_name("work", &entries).unwrap(), + "socket:/tmp/work.sock" + ); + assert_eq!( + resolve_remote_name("alias", &entries).unwrap(), + "socket:/tmp/work.sock" + ); + assert_eq!( + resolve_remote_name("secret", &entries).unwrap(), + "share:passphrase" + ); + } + + #[test] + fn remote_name_errors_are_specific() { + let entries = [ + remote("off", "ssh:host", true), + remote("via-off", "off", false), + remote("a", "b", false), + remote("b", "a", false), + remote("dangling", "missing", false), + remote("secret", "share:passphrase", true), + ]; + let unknown = resolve_remote_name("nope", &entries).unwrap_err(); + assert!(unknown.starts_with("unknown target 'nope'"), "{unknown}"); + let disabled = resolve_remote_name("off", &entries).unwrap_err(); + assert!(disabled.contains("'off' is disabled"), "{disabled}"); + let through = resolve_remote_name("via-off", &entries).unwrap_err(); + assert!(through.contains("'off' is disabled"), "{through}"); + let cycle = resolve_remote_name("a", &entries).unwrap_err(); + assert!(cycle.contains("cycle"), "{cycle}"); + let dangling = resolve_remote_name("dangling", &entries).unwrap_err(); + assert!(dangling.contains("'missing'"), "{dangling}"); + let secret = resolve_remote_name("secret", &entries).unwrap_err(); + assert!(!secret.contains("passphrase"), "{secret}"); + } } diff --git a/crates/cli/src/yas_remotes.rs b/crates/cli/src/yas_remotes.rs index 4cccbdc0e..18d5673fb 100644 --- a/crates/cli/src/yas_remotes.rs +++ b/crates/cli/src/yas_remotes.rs @@ -40,7 +40,10 @@ struct Catalogue { impl Catalogue { async fn open(on: Option<&str>, hub: &str) -> Result { - let mut client = NativeClient::connect(on, hub).await?; + Self::open_client(NativeClient::connect(on, hub).await?).await + } + + async fn open_client(mut client: NativeClient) -> Result { if !client.supports(family::KV, Class::Request, kv::request_kind::OPEN) { return Err( "this server does not offer the KV family, which is where remotes live".to_owned(), @@ -162,3 +165,16 @@ pub(crate) async fn read(on: Option<&str>, hub: &str) -> Result let mut catalogue = Catalogue::open(on, hub).await?; Ok(catalogue.read().await?.0) } + +/// The home server's catalogue, for resolving a bare target name. +/// +/// The home server is the local default instance (`YAS_SOCK`, else the +/// default socket), never the configured target: resolving `yas.target` +/// through itself would recurse, and the home server is where +/// `yas remote add` without `--on` writes. +pub(crate) async fn read_home() -> Result, String> { + let transport = crate::transport::connect_home().await?; + let client = NativeClient::connect_transport(transport, "yas-cli").await?; + let mut catalogue = Catalogue::open_client(client).await?; + Ok(catalogue.read().await?.0) +} diff --git a/docs/design/net.md b/docs/design/net.md index c9957fc8e..173b1b853 100644 --- a/docs/design/net.md +++ b/docs/design/net.md @@ -308,7 +308,7 @@ expectation. ### A named list: `yas.forwards` -The same shape as `yas.remotes` ([../README.md](../../README.md), +The same shape as the legacy `yas.remotes` file ([../README.md](../../README.md), [crates/webserver/src/config.rs](../../crates/webserver/src/config.rs)): its own ordered file at `~/.config/yas/yas.forwards`, `name = spec` per line, `#`-prefixed lines meaning **disabled but preserved**, mode 0600. `yas.conf` is a flat key→value map and cannot hold an ordered From cd80c67149498f0270724e2261dbe5896f97552d Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 18:30:06 +0000 Subject: [PATCH 09/19] Stop creating yas.remotes when reading it read_remotes_full wrote `local = local` to a missing yas.remotes, so a lookup created a file. A missing file now reads as an empty list. --- crates/webserver/src/config.rs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/webserver/src/config.rs b/crates/webserver/src/config.rs index cb5a1a833..6c626ebed 100644 --- a/crates/webserver/src/config.rs +++ b/crates/webserver/src/config.rs @@ -702,7 +702,7 @@ pub struct RemoteEntry { } /// Read `yas.remotes` and return ordered enabled `(name, uri)` pairs. -/// If the file does not exist, provisions it with `local = local` (0600). +/// A missing file is an empty list; reading never creates it. /// Disabled entries are filtered out — use [`read_remotes_full`] to keep them. pub fn read_remotes() -> Vec<(String, String)> { read_remotes_full() @@ -714,18 +714,13 @@ pub fn read_remotes() -> Vec<(String, String)> { /// Read `yas.remotes` including disabled entries. pub fn read_remotes_full() -> Vec { - let path = remotes_path(); - let contents = match std::fs::read_to_string(&path) { + read_remotes_at(&remotes_path()) +} + +fn read_remotes_at(path: &std::path::Path) -> Vec { + let contents = match std::fs::read_to_string(path) { Ok(c) => c, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - let default = vec![RemoteEntry { - name: "local".to_string(), - uri: "local".to_string(), - disabled: false, - }]; - write_remotes(&default); - return default; - } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return vec![], Err(e) => { eprintln!("yas: could not read {}: {e}", path.display()); return vec![]; @@ -1157,6 +1152,14 @@ fn parse_config_str(contents: &str) -> HashMap { mod tests { use super::*; + #[test] + fn reading_missing_remotes_file_does_not_create_it() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("yas.remotes"); + assert!(read_remotes_at(&path).is_empty()); + assert!(!path.exists()); + } + #[cfg(unix)] #[test] fn packaged_system_socket_prefers_private_user_directory() { From 7d427d97fba0fd19bf12c56cb8d82ee46bd4b4db Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 18:07:32 +0000 Subject: [PATCH 10/19] Return empty output for a sequence cursor past the end A SEQUENCE cursor beyond the terminal's output produced an OutputResult whose start lay after its next cursor. Encoding rejected it and the server answered Internal, so `yas terminal history ID --since SEQ` with a future cursor failed. seq_text now clamps such a cursor to the end and returns no text. --- crates/alacritty-driver/src/lib.rs | 7 +++++++ crates/alacritty-driver/tests/seq_cursor.rs | 23 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/crates/alacritty-driver/src/lib.rs b/crates/alacritty-driver/src/lib.rs index d39affca9..f5bf831cc 100644 --- a/crates/alacritty-driver/src/lib.rs +++ b/crates/alacritty-driver/src/lib.rs @@ -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; } diff --git a/crates/alacritty-driver/tests/seq_cursor.rs b/crates/alacritty-driver/tests/seq_cursor.rs index 9dc7b546f..6db850b79 100644 --- a/crates/alacritty-driver/tests/seq_cursor.rs +++ b/crates/alacritty-driver/tests/seq_cursor.rs @@ -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) + ); +} From a76205dc30ce505d1af265d8518b3a5efe707a95 Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 20:03:52 +0000 Subject: [PATCH 11/19] Document OUTPUT behavior for a cursor past the end --- docs/design/yas.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/design/yas.md b/docs/design/yas.md index c07f47f52..ff652aaa8 100644 --- a/docs/design/yas.md +++ b/docs/design/yas.md @@ -2010,7 +2010,9 @@ index; with it, `from_index` is the number of records skipped back from and PROBE 3. COMMAND uses `a=command_index,b=column`, LATEST_COMMAND requires `a=0`, and SEQUENCE/PROBE use `a=sequence,b=column`; OUTPUT flags are zero. Every OUTPUT next cursor is normalized to SEQUENCE with `a=next_seq` and -`b=next_col`, including COMMAND, LATEST_COMMAND, and PROBE results. +`b=next_col`, including COMMAND, LATEST_COMMAND, and PROBE results. A SEQUENCE +cursor past the end of the output returns empty text whose start and next +cursor are both the current end. WAIT kinds are OUTPUT 0, COMMAND 1, and LATEST_COMMAND 2, with zero flags. OUTPUT uses `a=sequence,b=column`, requires a nonempty needle, and returns an From 85223239415ab9f24a34166cb4c85f174d27c25d Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 20:03:29 +0000 Subject: [PATCH 12/19] Size terminal OUTPUT collection limits for the result framing The CLI capped the collected OUTPUT reply at the requested text size, but the reply is an encoded OutputResult with 36 bytes of fixed fields ahead of the text. The cursor probe asks for 1 byte of text, so every probe failed with "query returned 36 bytes; collection limit is 1". That broke `yas terminal wait --pattern` and `yas terminal history --since now` outright, and a full-size `output`/`history` page could trip the same check. The collection limit now adds the framing. With the probe working, `wait --pattern` again takes the cursor before reading and matches only output produced after the wait began. --- crates/cli/src/yas_terminal.rs | 36 ++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/crates/cli/src/yas_terminal.rs b/crates/cli/src/yas_terminal.rs index 4728e2f5b..a855f6842 100644 --- a/crates/cli/src/yas_terminal.rs +++ b/crates/cli/src/yas_terminal.rs @@ -19,6 +19,14 @@ const MAX_QUERY_BYTES: u64 = 64 * 1024 * 1024; const READ_PAGE_BYTES: u32 = 8 * 1024 * 1024; const STATE_CREDIT: u64 = 1024 * 1024; +/// OutputResult framing around its text: the fixed fields plus the text length. +const OUTPUT_RESULT_OVERHEAD: u64 = 36; + +/// Collection limit for an OUTPUT reply whose text is capped at `max_bytes`. +fn output_collection_limit(max_bytes: u32) -> u64 { + (u64::from(max_bytes.max(1)) + OUTPUT_RESULT_OVERHEAD).min(MAX_QUERY_BYTES) +} + pub(crate) async fn cmd_list(on: Option<&str>, hub: &str) -> Result<(), String> { let mut client = NativeClient::connect(on, hub).await?; let mut terminals = client @@ -530,7 +538,7 @@ pub(crate) async fn cmd_output( initial_receive_credit: QUERY_CREDIT, extensions: Extensions::default(), }, - u64::from(max_bytes).min(MAX_QUERY_BYTES), + output_collection_limit(max_bytes), Duration::from_secs(10), ) .await?; @@ -1213,7 +1221,7 @@ async fn cmd_since( initial_receive_credit: QUERY_CREDIT, extensions: Extensions::default(), }, - u64::from(request_max.max(1)).min(MAX_QUERY_BYTES), + output_collection_limit(request_max), Duration::from_secs(10), ) .await?; @@ -1262,7 +1270,7 @@ async fn probe_output_cursor( initial_receive_credit: QUERY_CREDIT, extensions: Extensions::default(), }, - 1, + output_collection_limit(1), Duration::from_secs(10), ) .await?; @@ -1299,7 +1307,7 @@ async fn wait_for_pattern( initial_receive_credit: QUERY_CREDIT, extensions: Extensions::default(), }, - u64::from(terminal_args::OUTPUT_MAX_BYTES), + output_collection_limit(terminal_args::OUTPUT_MAX_BYTES), Duration::from_secs(10), ) .await?; @@ -1711,4 +1719,24 @@ mod tests { assert_eq!(launch.environment[1].key, b"Z"); assert_eq!(launch.extensions.0[0].value, 3_000_000_000u64.to_le_bytes()); } + + #[test] + fn output_collection_limit_covers_result_framing() { + for text_len in [0usize, 1, 4096] { + let encoded = terminal::OutputResult { + generation: 1, + flags: 0, + start_seq: u64::MAX / 2, + start_col: 7, + next_seq: u64::MAX / 2, + next_col: 7 + text_len as u32, + text: vec![b'x'; text_len], + } + .encode() + .unwrap(); + let limit = output_collection_limit(text_len as u32); + assert!(encoded.len() as u64 <= limit, "{} > {limit}", encoded.len()); + } + assert_eq!(output_collection_limit(u32::MAX), MAX_QUERY_BYTES); + } } From a7f5a6d38214d76a13aff35f49e6dc6381fa2b9e Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 20:04:14 +0000 Subject: [PATCH 13/19] Make `yas terminal output --wait` time out with 124 A command wait that expired before any command it could report on had started answered NOT_FOUND, so `output --wait` without an index exited 1 with a NotFound error instead of the documented 124. The server now answers TIMEOUT in that case (NOT_FOUND stays for an exited terminal or an evicted index), and the CLI maps it to exit 124 with a "timed out" message. A command that is still running at the deadline also prints a timeout note alongside the existing 124. The index-less wait keeps waiting for the running command, or for the next one to start when none is running, so `send` followed by `output --wait` cannot race to the previous command's result. The help, learn.md, and design docs now say so. Adds a server test covering this and the past-end OUTPUT cursor. --- crates/cli/src/cli.rs | 8 +- crates/cli/src/learn.md | 2 + crates/cli/src/yas_native.rs | 2 +- crates/cli/src/yas_terminal.rs | 85 +++++++++---- crates/server/src/yas.rs | 138 ++++++++++++++++++++++ crates/server/src/yas_terminal_queries.rs | 12 +- docs/design/term-journal.md | 12 +- docs/design/yas.md | 7 +- 8 files changed, 232 insertions(+), 34 deletions(-) diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 91a1e688b..805cd1631 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -1025,9 +1025,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 diff --git a/crates/cli/src/learn.md b/crates/cli/src/learn.md index f69e1a74d..0f453eb17 100644 --- a/crates/cli/src/learn.md +++ b/crates/cli/src/learn.md @@ -142,6 +142,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 diff --git a/crates/cli/src/yas_native.rs b/crates/cli/src/yas_native.rs index 530b9f197..75aef4545 100644 --- a/crates/cli/src/yas_native.rs +++ b/crates/cli/src/yas_native.rs @@ -1388,7 +1388,7 @@ fn wire_error(error: yas_wire::Error) -> String { format!("YAS wire error: {error}") } -fn format_result_detail(detail: &Extensions) -> String { +pub(crate) fn format_result_detail(detail: &Extensions) -> String { if detail.0.is_empty() { "no detail".to_string() } else { diff --git a/crates/cli/src/yas_terminal.rs b/crates/cli/src/yas_terminal.rs index a855f6842..cf161a86c 100644 --- a/crates/cli/src/yas_terminal.rs +++ b/crates/cli/src/yas_terminal.rs @@ -6,12 +6,17 @@ pub(crate) mod stream; use std::time::Duration; use yas_wire::{ - Class, Decode, Encode, Extension, Extensions, family, + Class, Decode, Encode, Extension, Extensions, + core::Status, + family, state::{Phase, RecordKind, StateAck, StateEvent, Watch, WatchResult}, terminal, }; -use crate::{terminal_args, yas_native::NativeClient}; +use crate::{ + terminal_args, + yas_native::{NativeClient, format_result_detail}, +}; const NS_PER_SECOND: u64 = 1_000_000_000; const QUERY_CREDIT: u64 = 1024 * 1024; @@ -486,26 +491,43 @@ pub(crate) async fn cmd_output( } else { yas_wire::schema::terminal::WAIT_LATEST_COMMAND as u8 }; - let query = terminal_query( - &mut client, - terminal::request_kind::WAIT, - &terminal::Wait { - terminal_handle: terminal_record.terminal_handle, - generation: terminal_record.generation, - wait_kind, - flags: yas_wire::schema::terminal::WAIT_FLAGS as u8, - cursor_a: index.unwrap_or(0), - cursor_b: 0, - max_bytes, - timeout_ns: timeout.saturating_mul(NS_PER_SECOND).max(1), - needle: Vec::new(), - initial_receive_credit: QUERY_CREDIT, - extensions: Extensions::default(), - }, - MAX_QUERY_BYTES, - Duration::from_secs(timeout.saturating_add(2)), - ) - .await?; + let request = terminal::Wait { + terminal_handle: terminal_record.terminal_handle, + generation: terminal_record.generation, + wait_kind, + flags: yas_wire::schema::terminal::WAIT_FLAGS as u8, + cursor_a: index.unwrap_or(0), + cursor_b: 0, + max_bytes, + timeout_ns: timeout.saturating_mul(NS_PER_SECOND).max(1), + needle: Vec::new(), + initial_receive_credit: QUERY_CREDIT, + extensions: Extensions::default(), + }; + let prefix = client + .request_result_with_timeout( + family::TERMINAL, + terminal::request_kind::WAIT, + request.encode().map_err(wire_error)?, + true, + Duration::from_secs(timeout.saturating_add(2)), + ) + .await?; + match prefix.status { + Status::Ok => {} + Status::Timeout => { + eprintln!("yas: timed out waiting for a command in pty {id}"); + return Ok(124); + } + Status::NotFound => return Err(format!("pty {id}: no such command")), + status => { + return Err(format!( + "YAS Terminal WAIT failed with {status:?}: {}", + format_result_detail(&prefix.detail) + )); + } + } + let query = collect_query(&mut client, &prefix.body, MAX_QUERY_BYTES).await?; expect_content(&query, yas_wire::schema::terminal::CONTENT_JOURNAL as u8)?; let result = terminal::JournalResult::decode(&query_bytes(query).await?).map_err(wire_error)?; @@ -579,6 +601,15 @@ pub(crate) async fn cmd_output( ); } } + if let Some(record) = waited.as_ref().filter(|record| { + journal_running(record) + && record.flags & yas_wire::schema::terminal::JOURNAL_PTY_EXITED as u16 == 0 + }) { + eprintln!( + "yas: timed out waiting for command {} in pty {id}", + record.index + ); + } Ok(waited.as_ref().map_or(0, journal_exit_code)) } @@ -760,7 +791,15 @@ async fn terminal_query( timeout, ) .await?; - let query = terminal::QueryBody::decode(&body).map_err(wire_error)?; + collect_query(client, &body, maximum).await +} + +async fn collect_query( + client: &mut NativeClient, + body: &[u8], + maximum: u64, +) -> Result { + let query = terminal::QueryBody::decode(body).map_err(wire_error)?; query .validate_receive_credit(QUERY_CREDIT) .map_err(wire_error)?; diff --git a/crates/server/src/yas.rs b/crates/server/src/yas.rs index 7f6d527de..799071ef4 100644 --- a/crates/server/src/yas.rs +++ b/crates/server/src/yas.rs @@ -6066,6 +6066,7 @@ const fn terminal_query_status(error: &super::yas_terminal_queries::Error) -> St super::yas_terminal_queries::Error::NotFound => Status::NotFound, super::yas_terminal_queries::Error::Stale => Status::Stale, super::yas_terminal_queries::Error::TooLarge => Status::ResourceExhausted, + super::yas_terminal_queries::Error::Timeout => Status::Timeout, super::yas_terminal_queries::Error::Internal => Status::Internal, } } @@ -52087,6 +52088,143 @@ mod tests { delivery.abort(); } + #[cfg(unix)] + #[tokio::test(flavor = "multi_thread")] + async fn terminal_output_past_end_is_empty_and_command_wait_times_out() { + let state = super::super::tests::process_transport::test_state( + super::super::process::Server::new(false, true), + ); + let delivery_state = state.clone(); + let delivery = tokio::spawn(async move { + loop { + tokio::select! { + _ = delivery_state.delivery_notify.notified() => {} + _ = tokio::time::sleep(Duration::from_millis(10)) => {} + } + let _ = super::super::tick(&delivery_state).await; + } + }); + let (mut client, codec, _, server_task) = + start_registered_session(state, &[family::TRANSFER, family::TERMINAL]).await; + write_request( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::CREATE, + 10, + &yas_terminal::Create { + rows: 24, + cols: 80, + operation_id: [0x43; 16], + launch: yas_terminal::Launch { + command: yas_terminal::Command::Argv(vec![ + b"/bin/sh".to_vec(), + b"-c".to_vec(), + b"printf 'ready\\n'; exec cat".to_vec(), + ]), + cwd: yas_terminal::Cwd::ServerDefault, + environment_base: yas_terminal::EnvironmentBase::Server, + environment: Vec::new(), + extensions: Extensions::default(), + }, + extensions: Extensions::default(), + }, + ) + .await; + let mut frames = Vec::new(); + let created = next_terminal_result( + &mut client, + &codec, + yas_wire::schema::terminal::request::CREATE, + 10, + &mut frames, + ) + .await; + assert_eq!(created.status, Status::Ok); + let created = yas_terminal::CreateResult::decode(&created.body).unwrap(); + + write_request( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::OUTPUT, + 11, + &yas_terminal::Output { + terminal_handle: created.terminal_handle, + generation: created.generation, + cursor_kind: yas_wire::schema::terminal::OUTPUT_CURSOR_SEQUENCE as u8, + flags: yas_wire::schema::terminal::OUTPUT_REQUEST_FLAGS as u8, + cursor_a: 1_000_000, + cursor_b: 0, + max_bytes: 64 * 1024, + initial_receive_credit: 64 * 1024, + extensions: Extensions::default(), + }, + ) + .await; + let output = next_sensitive_result( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::OUTPUT, + 11, + ) + .await; + assert_eq!(output.status, Status::Ok); + let yas_terminal::QueryDelivery::Inline(bytes) = + yas_terminal::QueryBody::decode(&output.body) + .unwrap() + .delivery + else { + panic!("an empty OUTPUT answer is inline"); + }; + let output = yas_terminal::OutputResult::decode(&bytes).unwrap(); + assert!(output.text.is_empty()); + assert!(output.next_seq < 1_000_000); + assert_eq!( + (output.start_seq, output.start_col), + (output.next_seq, output.next_col) + ); + + write_request( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::WAIT, + 12, + &yas_terminal::Wait { + terminal_handle: created.terminal_handle, + generation: created.generation, + wait_kind: yas_wire::schema::terminal::WAIT_LATEST_COMMAND as u8, + flags: yas_wire::schema::terminal::WAIT_FLAGS as u8, + cursor_a: 0, + cursor_b: 0, + max_bytes: 64 * 1024, + timeout_ns: 200_000_000, + needle: Vec::new(), + initial_receive_credit: 64 * 1024, + extensions: Extensions::default(), + }, + ) + .await; + assert_eq!( + next_sensitive_result( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::WAIT, + 12, + ) + .await + .status, + Status::Timeout, + ); + + drop(client); + timeout(TEST_TIMEOUT, server_task).await.unwrap().unwrap(); + delivery.abort(); + } + #[cfg(unix)] #[tokio::test(flavor = "multi_thread")] async fn terminal_cwd_rejects_under_credit_and_remains_live_at_exact_credit() { diff --git a/crates/server/src/yas_terminal_queries.rs b/crates/server/src/yas_terminal_queries.rs index 14db4914d..e51bcf395 100644 --- a/crates/server/src/yas_terminal_queries.rs +++ b/crates/server/src/yas_terminal_queries.rs @@ -49,6 +49,9 @@ pub(crate) enum Error { NotFound, Stale, TooLarge, + /// A command wait ran out of time before any command it could report on + /// had started. + Timeout, Internal, } @@ -983,8 +986,13 @@ fn poll_command_wait(pty: &Pty, index: u64, timed_out: bool) -> Result Date: Mon, 28 Sep 2026 20:04:14 +0000 Subject: [PATCH 14/19] Tolerate a closed stdout in terminal commands `yas terminal start --wait CMD | head -1` panicked with "failed printing to stdout: Broken pipe" once head exited, because the terminal commands printed with println!. They now write through a helper that treats EPIPE as success, so the command finishes normally and exits with its own status (the command's exit code for `start --wait`). Other stdout errors are reported on stderr. --- crates/cli/src/yas_terminal.rs | 58 ++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/crates/cli/src/yas_terminal.rs b/crates/cli/src/yas_terminal.rs index cf161a86c..d4708e653 100644 --- a/crates/cli/src/yas_terminal.rs +++ b/crates/cli/src/yas_terminal.rs @@ -24,6 +24,27 @@ const MAX_QUERY_BYTES: u64 = 64 * 1024 * 1024; const READ_PAGE_BYTES: u32 = 8 * 1024 * 1024; const STATE_CREDIT: u64 = 1024 * 1024; +/// Like `println!`, but a closed stdout (`yas terminal list | head -1`) is +/// not an error: the command still finishes and exits with its own status. +macro_rules! outln { + () => { + write_stdout(b"\n") + }; + ($($arg:tt)*) => { + write_stdout(format!("{}\n", format_args!($($arg)*)).as_bytes()) + }; +} + +fn write_stdout(bytes: &[u8]) { + use std::io::Write; + let mut stdout = std::io::stdout().lock(); + if let Err(error) = stdout.write_all(bytes).and_then(|()| stdout.flush()) + && error.kind() != std::io::ErrorKind::BrokenPipe + { + eprintln!("yas: cannot write to stdout: {error}"); + } +} + /// OutputResult framing around its text: the fixed fields plus the text length. const OUTPUT_RESULT_OVERHEAD: u64 = 36; @@ -43,7 +64,7 @@ pub(crate) async fn cmd_list(on: Option<&str>, hub: &str) -> Result<(), String> .collect::>(); terminals.sort_by_key(|record| record.terminal_handle); - println!("ID\tTAG\tTITLE\tCOMMAND\tCWD\tSTATUS"); + outln!("ID\tTAG\tTITLE\tCOMMAND\tCWD\tSTATUS"); for record in terminals { let id = terminal_id(record.terminal_handle)?; let tag = text_extension( @@ -68,7 +89,7 @@ pub(crate) async fn cmd_list(on: Option<&str>, hub: &str) -> Result<(), String> .map(|value| String::from_utf8_lossy(value).into_owned()) .unwrap_or_default(); let status = terminal_status(&record)?; - println!("{id}\t{tag}\t{title}\t{command}\t{cwd}\t{status}"); + outln!("{id}\t{tag}\t{title}\t{command}\t{cwd}\t{status}"); } Ok(()) } @@ -104,7 +125,7 @@ pub(crate) async fn cmd_start( ) .await?; let id = terminal_id(result.terminal_handle)?; - println!("{id}"); + outln!("{id}"); Ok(id) } @@ -390,10 +411,9 @@ pub(crate) async fn cmd_cwd(on: Option<&str>, hub: &str, id: u64) -> Result<(), .await?; expect_content(&query, yas_wire::schema::terminal::CONTENT_PATH as u8)?; let bytes = query_bytes(query).await?; - std::io::Write::write_all(&mut std::io::stdout(), &bytes) - .map_err(|error| format!("cannot write terminal cwd: {error}"))?; + write_stdout(&bytes); if !bytes.ends_with(b"\n") { - println!(); + outln!(); } Ok(()) } @@ -409,7 +429,7 @@ pub(crate) async fn cmd_journal( let mut client = NativeClient::connect(on, hub).await?; let record = find_terminal(&mut client, id).await?; if !json { - println!("INDEX\tSTATUS\tEXIT\tMS\tSTART_SEQ\tEND_SEQ\tCOMMAND"); + outln!("INDEX\tSTATUS\tEXIT\tMS\tSTART_SEQ\tEND_SEQ\tCOMMAND"); } let mut cursor = from.unwrap_or(0); let mut tail = from.is_none(); @@ -442,7 +462,7 @@ pub(crate) async fn cmd_journal( terminal::JournalResult::decode(&query_bytes(query).await?).map_err(wire_error)?; for entry in &journal.records { if json { - println!("{}", journal_record_json(entry)); + outln!("{}", journal_record_json(entry)); } else { print_journal_record(entry); } @@ -584,12 +604,11 @@ pub(crate) async fn cmd_output( "next_cursor".into(), format_cursor(output.next_seq, output.next_col).into(), ); - println!("{value}"); + outln!("{value}"); } else { - std::io::Write::write_all(&mut std::io::stdout(), &output.text) - .map_err(|error| format!("cannot write terminal output: {error}"))?; + write_stdout(&output.text); if !output.text.is_empty() && !output.text.ends_with(b"\n") { - println!(); + outln!(); } if evicted { eprintln!("yas: output start had scrolled out of the backlog"); @@ -958,7 +977,7 @@ fn print_query_rows_with_separator( .map_err(|_| "YAS Terminal returned non-UTF-8 text".to_string())? }; if !*first && !text.is_empty() { - println!(); + outln!(); } print!("{text}"); *first = false; @@ -1177,7 +1196,7 @@ fn journal_record_json(record: &terminal::JournalRecord) -> serde_json::Value { fn print_journal_record(record: &terminal::JournalRecord) { let exit = journal_exit(record).map(|value| value.to_string()); let duration = journal_duration(record).map(|value| value.to_string()); - println!( + outln!( "{}\t{}\t{}\t{}\t{}\t{}\t{}", record.index, journal_status(record), @@ -1268,7 +1287,7 @@ async fn cmd_since( let output = terminal::OutputResult::decode(&query_bytes(query).await?).map_err(wire_error)?; let next = format_cursor(output.next_seq, output.next_col); if json { - println!( + outln!( "{}", serde_json::json!({ "pty": id, @@ -1281,10 +1300,9 @@ async fn cmd_since( }) ); } else { - std::io::Write::write_all(&mut std::io::stdout(), &output.text) - .map_err(|error| format!("cannot write terminal history: {error}"))?; + write_stdout(&output.text); if !output.text.is_empty() && !output.text.ends_with(b"\n") { - println!(); + outln!(); } eprintln!("cursor: {next}"); } @@ -1355,7 +1373,7 @@ async fn wait_for_pattern( (sequence, column) = (output.next_seq, output.next_col); pending.push_str(&String::from_utf8_lossy(&output.text)); if let Some(line) = first_matching_line(&mut pending, regex) { - println!("{line}"); + outln!("{line}"); return Ok(0); } if output.flags & yas_wire::schema::terminal::OUTPUT_TRUNCATED as u16 != 0 { @@ -1393,7 +1411,7 @@ fn first_matching_line(pending: &mut String, regex: ®ex::Regex) -> Option Result { - println!("{}", terminal_status(record)?); + outln!("{}", terminal_status(record)?); terminal_exit_code(record) } From f77d8282898b41c717539bd387201846e939cf48 Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 20:04:14 +0000 Subject: [PATCH 15/19] Refuse `yas terminal send` to an exited or unknown terminal WRITE is a fire-and-forget event, so sending to an exited terminal (or a nonexistent id) silently did nothing and exited 0. The CLI now looks the terminal up first and fails with a clear error and exit 1 when it does not exist or has exited, pointing at `yas terminal restart`. --- crates/cli/src/cli.rs | 1 + crates/cli/src/learn.md | 2 ++ crates/cli/src/yas_terminal.rs | 9 ++++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 805cd1631..c2df945a2 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -1285,6 +1285,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: diff --git a/crates/cli/src/learn.md b/crates/cli/src/learn.md index 0f453eb17..4612f7b56 100644 --- a/crates/cli/src/learn.md +++ b/crates/cli/src/learn.md @@ -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 diff --git a/crates/cli/src/yas_terminal.rs b/crates/cli/src/yas_terminal.rs index d4708e653..e789cf2c9 100644 --- a/crates/cli/src/yas_terminal.rs +++ b/crates/cli/src/yas_terminal.rs @@ -140,12 +140,19 @@ pub(crate) async fn cmd_send( return Ok(()); } let mut client = NativeClient::connect(on, hub).await?; + let record = find_terminal(&mut client, id).await?; + if record.lifecycle == terminal::Lifecycle::Exited { + return Err(format!( + "cannot send to pty {id}: it has {} (`yas terminal restart {id}` re-runs it)", + terminal_status(&record)? + )); + } client .send_typed_event( family::TERMINAL, terminal::event_kind::WRITE, &terminal::Write { - terminal_handle: terminal_handle(id)?, + terminal_handle: record.terminal_handle, data: bytes, }, true, From c1b9050034949d5017ef40e7e913d76ae504fd52 Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 19:14:40 +0000 Subject: [PATCH 16/19] Fix terminal restart, server env flags, and missing-program status Restart: the replayable launch record lived in a per-connection map, so RESTART with REPLAY only worked on the connection that created the terminal. Every CLI command opens a new connection, so `yas terminal restart` always answered Unsupported. The launch record now lives on the terminal itself and is replaced by a successful REPLACE restart. Environment: --export-sock / YAS_EXPORT_SOCK and --inject-path / YAS_INJECT_PATH only applied to the Derived environment base, which native launches never use. They now apply to the Server base as well. A Server-base terminal also no longer inherits a YAS_SOCK that names a different server (for example when the server was started from a shell inside another YAS server); an inherited YAS_SOCK equal to this server's own socket is kept, and --export-sock always sets it. Missing program: a CREATE whose argv program is not on the child's PATH answered ResourceExhausted, and a RESTART with one answered Io. Both now answer NotFound, and `yas terminal start` prints "PROGRAM: command not found". --- crates/cli/src/cli.rs | 3 +- crates/cli/src/yas_terminal.rs | 19 +- crates/server/src/lib.rs | 9 +- crates/server/src/pty/pty_unix.rs | 198 ++++++++++++++++---- crates/server/src/pty/pty_windows.rs | 11 ++ crates/server/src/yas.rs | 259 ++++++++++++++++++++++----- docs/design/yas.md | 3 + docs/server.md | 4 + 8 files changed, 418 insertions(+), 88 deletions(-) diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index c2df945a2..3fbb0c3c5 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -600,7 +600,8 @@ pub enum Command { fd_channel: Option, /// 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, diff --git a/crates/cli/src/yas_terminal.rs b/crates/cli/src/yas_terminal.rs index e789cf2c9..3fd82f06e 100644 --- a/crates/cli/src/yas_terminal.rs +++ b/crates/cli/src/yas_terminal.rs @@ -116,14 +116,27 @@ pub(crate) async fn cmd_start( extensions: Extensions(create_extensions), }; let mut client = NativeClient::connect(on, hub).await?; - let result: terminal::CreateResult = client - .request_typed( + let prefix = client + .request_result( family::TERMINAL, terminal::request_kind::CREATE, - &create, + create.encode().map_err(wire_error)?, true, ) .await?; + match prefix.status { + Status::Ok => {} + Status::NotFound if !request.shell && !request.command.is_empty() => { + return Err(format!("{}: command not found", request.command[0])); + } + status => { + return Err(format!( + "YAS Terminal CREATE failed with {status:?}: {}", + format_result_detail(&prefix.detail) + )); + } + } + let result = terminal::CreateResult::decode(&prefix.body).map_err(wire_error)?; let id = terminal_id(result.terminal_handle)?; outln!("{id}"); Ok(id) diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 14906cc8e..8c817423a 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -462,8 +462,9 @@ pub struct Config { /// Skip compositor initialization (e.g. for share-only mode). pub skip_compositor: bool, /// Export the server's IPC path as `YAS_SOCK` in spawned terminals so - /// `yas` invocations inside them target this server. Off by default: - /// `YAS_*` is otherwise stripped from child environments. + /// `yas` invocations inside them target this server. Off by default, + /// though an inherited `YAS_SOCK` naming another server is dropped either + /// way. pub export_sock: bool, /// Append the directory holding the running server binary to `PATH` in /// spawned terminals, so `yas` is callable inside them (Unix only; the @@ -1075,6 +1076,10 @@ struct Pty { /// this a terminal created with an argv, an environment, or both came back /// from a restart as a bare login shell. spec: pty::OwnedChildSpec, + /// The native YAS launch this terminal was created or last replaced + /// with. Terminal Restart replays it for any client that can see the + /// terminal, not only the connection that created it. + native_launch: Option, /// Working directory last reported by the shell via OSC 7, already /// validated by `parse_osc7_url` (docs/protocol.md, "Working directory /// tracking"). Last write wins; None until shell integration first diff --git a/crates/server/src/pty/pty_unix.rs b/crates/server/src/pty/pty_unix.rs index 2e3daf8fa..2bbca4d55 100644 --- a/crates/server/src/pty/pty_unix.rs +++ b/crates/server/src/pty/pty_unix.rs @@ -41,9 +41,10 @@ pub struct ChildSpec<'a> { /// /// `Derived` preserves the default YAS terminal behavior. `Server` and /// `Empty` are the native YAS launch variants. `Server` receives the live -/// session environment before the request's exact entries; `Empty` remains -/// byte-for-byte exact. Neither receives terminal, PATH, locale, or YAS -/// variables unless the launch explicitly supplies them. +/// session environment and the server's `YAS_SOCK`/`PATH` rules (see +/// [`build_child_env`]) before the request's exact entries; `Empty` remains +/// byte-for-byte exact. Neither receives terminal or locale variables unless +/// the launch explicitly supplies them. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum ChildEnvironmentBase { #[default] @@ -114,9 +115,16 @@ impl ChildSpec<'_> { /// client entry always wins, whichever layer would otherwise have set the key. /// This is the precedence the process family already documents for /// `PROCESS_SPAWN` (`command_for` in `process.rs`). +/// +/// `yas_sock` is set only when the server exports its socket; `own_sock` is +/// the path it listens on either way. A `Server` base never forwards an +/// inherited `YAS_SOCK` naming some other server, since `yas` inside the +/// terminal would then drive that server instead of this one. +#[allow(clippy::too_many_arguments)] fn build_child_env( session_env: Option<&crate::app_env::SessionEnv>, yas_sock: Option<&str>, + own_sock: Option<&str>, path_dir: Option<&str>, overrides: &[(String, String)], removals: &[String], @@ -158,6 +166,9 @@ fn build_child_env( env.push((key.as_bytes().to_vec(), value.as_bytes().to_vec())); } } + if base == ChildEnvironmentBase::Server { + apply_server_yas_env(&mut env, yas_sock, own_sock, path_dir); + } // The request is the final authority, including over session values. for (key, value) in entries { env.retain(|(candidate, _)| candidate != key); @@ -254,6 +265,78 @@ fn build_child_env( .collect() } +/// The `YAS_SOCK` and `PATH` rules for a `Server` base environment; see +/// [`build_child_env`]. +fn apply_server_yas_env( + env: &mut Vec<(Vec, Vec)>, + yas_sock: Option<&str>, + own_sock: Option<&str>, + path_dir: Option<&str>, +) { + env.retain(|(key, value)| { + key.as_slice() != b"YAS_SOCK" + || own_sock.is_some_and(|own| value.as_slice() == own.as_bytes()) + }); + if let Some(sock) = yas_sock { + env.retain(|(key, _)| key.as_slice() != b"YAS_SOCK"); + env.push((b"YAS_SOCK".to_vec(), sock.as_bytes().to_vec())); + } + if let Some(dir) = path_dir { + match env.iter_mut().find(|(key, _)| key.as_slice() == b"PATH") { + Some((_, value)) => { + if !value + .split(|byte| *byte == b':') + .any(|entry| entry == dir.as_bytes()) + { + if !value.is_empty() { + value.push(b':'); + } + value.extend_from_slice(dir.as_bytes()); + } + } + None => env.push((b"PATH".to_vec(), dir.as_bytes().to_vec())), + } + } +} + +/// The environment a terminal child of this server is started with. +fn spawn_child_env( + spec: &ChildSpec<'_>, + state: &AppState, + session_env: Option<&crate::app_env::SessionEnv>, +) -> Vec { + let yas_sock = state + .config + .export_sock + .then(|| state.config.ipc_path.as_str()); + let path_dir = state.config.inject_path.then(exe_dir).flatten(); + build_child_env( + session_env, + yas_sock, + Some(state.config.ipc_path.as_str()), + path_dir, + spec.env, + spec.env_remove, + spec.environment_base, + spec.exact_env, + ) +} + +/// Whether `spec` names an argv program that cannot be found on the PATH the +/// child would run with, so a failed spawn can be reported as not found +/// rather than as a generic failure. +pub fn launch_program_missing( + spec: ChildSpec<'_>, + state: &AppState, + session_env: Option<&crate::app_env::SessionEnv>, +) -> bool { + let Some(program) = spec.argv.and_then(|argv| argv.first()) else { + return false; + }; + let env = spawn_child_env(&spec, state, session_env); + resolve_in_path(program, child_path(&env).as_deref()).is_none() +} + /// Everything the child needs to `execve`, built entirely before `fork()`. /// /// Nothing here may be deferred to the child: after fork in a multi-threaded @@ -1002,20 +1085,7 @@ pub fn spawn_pty( // Build the child's environment before fork() to avoid calling // set_var/remove_var after fork in a multi-threaded process (UB per POSIX). - let yas_sock = state - .config - .export_sock - .then(|| state.config.ipc_path.as_str()); - let path_dir = state.config.inject_path.then(exe_dir).flatten(); - let child_env = build_child_env( - session_env, - yas_sock, - path_dir, - spec.env, - spec.env_remove, - spec.environment_base, - spec.exact_env, - ); + let child_env = spawn_child_env(&spec, &state, session_env); let child_envp: Vec<*const libc::c_char> = child_env .iter() .map(|c| c.as_ptr()) @@ -1144,6 +1214,7 @@ pub fn spawn_pty( exit_status: yas_terminal_model::EXIT_STATUS_UNKNOWN, command: list_command.map(str::to_owned), spec: spec.to_owned_spec(), + native_launch: None, osc7_cwd: None, journal: crate::journal::CommandJournal::default(), osc_carry: Vec::new(), @@ -1184,20 +1255,7 @@ pub fn respawn_child( } // Build the child's environment before fork() (same rationale as spawn_pty). - let yas_sock = state - .config - .export_sock - .then(|| state.config.ipc_path.as_str()); - let path_dir = state.config.inject_path.then(exe_dir).flatten(); - let child_env = build_child_env( - session_env, - yas_sock, - path_dir, - spec.env, - spec.env_remove, - spec.environment_base, - spec.exact_env, - ); + let child_env = spawn_child_env(&spec, &state, session_env); let child_envp: Vec<*const libc::c_char> = child_env .iter() .map(|c| c.as_ptr()) @@ -1286,8 +1344,8 @@ pub fn respawn_child( #[cfg(test)] mod tests { use super::{ - ChildEnvironmentBase, ChildSpec, PtyHandle, build_child_env, child_path, - collect_exit_status, plan_exec, reap_zombies, resolve_in_path, + ChildEnvironmentBase, ChildSpec, PtyHandle, apply_server_yas_env, build_child_env, + child_path, collect_exit_status, plan_exec, reap_zombies, resolve_in_path, }; use std::collections::HashMap; use std::ffi::CString; @@ -1476,6 +1534,7 @@ mod tests { build_child_env( Some(&session_env), yas_sock, + None, path_dir, &[], &[], @@ -1873,6 +1932,7 @@ mod tests { Some(&session_env), None, None, + None, &[], &[], ChildEnvironmentBase::Empty, @@ -1906,6 +1966,7 @@ mod tests { Some(&session_env), None, None, + None, &[], &[], ChildEnvironmentBase::Server, @@ -1951,6 +2012,7 @@ mod tests { Some(&session_env), Some("/tmp/yas-test/ipc.sock"), None, + None, &overrides(&[ // A plain addition. ("YAS_PROBE", "hello"), @@ -1991,6 +2053,7 @@ mod tests { None, None, None, + None, &overrides(&[("PATH", dir.to_str().unwrap())]), &[], ChildEnvironmentBase::Derived, @@ -2152,6 +2215,75 @@ mod tests { ); } + fn server_env( + inherited: &[(&str, &str)], + yas_sock: Option<&str>, + own_sock: Option<&str>, + path_dir: Option<&str>, + ) -> HashMap { + let mut env = inherited + .iter() + .map(|(key, value)| (key.as_bytes().to_vec(), value.as_bytes().to_vec())) + .collect(); + apply_server_yas_env(&mut env, yas_sock, own_sock, path_dir); + env.into_iter() + .map(|(key, value)| { + ( + String::from_utf8(key).unwrap(), + String::from_utf8(value).unwrap(), + ) + }) + .collect() + } + + #[test] + fn server_env_drops_a_yas_sock_naming_another_server() { + let env = server_env( + &[("YAS_SOCK", "/run/other.sock")], + None, + Some("/run/own.sock"), + None, + ); + assert!(!env.contains_key("YAS_SOCK")); + + let env = server_env( + &[("YAS_SOCK", "/run/own.sock")], + None, + Some("/run/own.sock"), + None, + ); + assert_eq!( + env.get("YAS_SOCK").map(String::as_str), + Some("/run/own.sock") + ); + + let env = server_env( + &[("YAS_SOCK", "/run/other.sock")], + Some("/run/own.sock"), + Some("/run/own.sock"), + None, + ); + assert_eq!( + env.get("YAS_SOCK").map(String::as_str), + Some("/run/own.sock") + ); + } + + #[test] + fn server_env_appends_the_binary_dir_to_path_once() { + let env = server_env(&[("PATH", "/usr/bin:/bin")], None, None, Some("/opt/yas")); + assert_eq!( + env.get("PATH").map(String::as_str), + Some("/usr/bin:/bin:/opt/yas") + ); + let env = server_env(&[("PATH", "/opt/yas:/bin")], None, None, Some("/opt/yas")); + assert_eq!(env.get("PATH").map(String::as_str), Some("/opt/yas:/bin")); + let env = server_env(&[], None, None, Some("/opt/yas")); + assert_eq!(env.get("PATH").map(String::as_str), Some("/opt/yas")); + let env = server_env(&[("PATH", "/bin")], None, None, None); + assert_eq!(env.get("PATH").map(String::as_str), Some("/bin")); + } + #[test] fn child_env_exports_yas_sock_only_when_requested() { let env = child_env_map(session_child_env(None, None, None, None, None, None, None)); diff --git a/crates/server/src/pty/pty_windows.rs b/crates/server/src/pty/pty_windows.rs index 36a8db838..a76116da3 100644 --- a/crates/server/src/pty/pty_windows.rs +++ b/crates/server/src/pty/pty_windows.rs @@ -513,6 +513,16 @@ fn exact_environment_block(spec: &ChildSpec<'_>) -> Option> { Some(block) } +/// Windows resolves the program inside `CreateProcessW`, so a missing one is +/// not distinguished from any other spawn failure. +pub fn launch_program_missing( + _spec: ChildSpec<'_>, + _state: &AppState, + _session_env: Option<&crate::app_env::SessionEnv>, +) -> bool { + false +} + #[allow(clippy::too_many_arguments)] pub fn spawn_pty( shell: &str, @@ -692,6 +702,7 @@ pub fn spawn_pty( exit_status: yas_terminal_model::EXIT_STATUS_UNKNOWN, command: list_command.map(str::to_owned), spec: spec.to_owned_spec(), + native_launch: None, osc7_cwd: None, journal: crate::journal::CommandJournal::default(), osc_carry: Vec::new(), diff --git a/crates/server/src/yas.rs b/crates/server/src/yas.rs index 799071ef4..148bb0638 100644 --- a/crates/server/src/yas.rs +++ b/crates/server/src/yas.rs @@ -3849,9 +3849,6 @@ impl TerminalCatalogue { struct TerminalRuntime { catalogue: TerminalCatalogue, app_handles: HashMap, - /// Exact native launch records. Terminals created outside this connection - /// predate its launch catalogue and intentionally have no replayable launch. - launches: HashMap, operations: HashMap<[u8; 16], TerminalOperationReplay>, next_view_id: u32, views: HashMap, @@ -4956,7 +4953,6 @@ impl TerminalRuntime { Self { catalogue, app_handles: HashMap::new(), - launches: HashMap::new(), operations: HashMap::new(), next_view_id: 1, views: HashMap::new(), @@ -14473,35 +14469,50 @@ impl Session { // engine initializes this field at zero, so creation sets // the first public generation explicitly. pty.generation = 1; + pty.native_launch = Some(request.launch.clone()); pty.deadline = prepared.deadline_after_ns.and_then(|ns| { std::time::Instant::now().checked_add(Duration::from_nanos(ns)) }); } - pty.and_then(|pty| { - let lifecycle = session.note_terminal_created(pty_id, pty.generation)?; - debug_assert_eq!(lifecycle.backend_id(), pty_id); - debug_assert_eq!(lifecycle.generation(), pty.generation); - let handle = lifecycle.terminal_handle(); - session.ptys.insert(pty_id, pty); - Some((handle, pty_id)) - }) + match pty { + Some(pty) => session + .note_terminal_created(pty_id, pty.generation) + .map(|lifecycle| { + debug_assert_eq!(lifecycle.backend_id(), pty_id); + debug_assert_eq!(lifecycle.generation(), pty.generation); + let handle = lifecycle.terminal_handle(); + session.ptys.insert(pty_id, pty); + (handle, pty_id) + }) + .ok_or(Status::ResourceExhausted), + None if super::pty::launch_program_missing( + prepared.spec.borrowed(&argv), + &state, + session_env.as_ref(), + ) => + { + Err(Status::NotFound) + } + None => Err(Status::ResourceExhausted), + } } else { - None + Err(Status::ResourceExhausted) } }; - let Some((terminal_handle, pty_id)) = created else { - self.record_terminal_replay( - yas_wire::schema::terminal::request::CREATE, - request.operation_id, - fingerprint, - Status::ResourceExhausted, - Vec::new(), - None, - None, - ); - return self - .send_sensitive_result(&frame, Status::ResourceExhausted, Vec::new()) - .await; + let (terminal_handle, pty_id) = match created { + Ok(created) => created, + Err(status) => { + self.record_terminal_replay( + yas_wire::schema::terminal::request::CREATE, + request.operation_id, + fingerprint, + status, + Vec::new(), + None, + None, + ); + return self.send_sensitive_result(&frame, status, Vec::new()).await; + } }; let initial_view_result = if let Some(initial_view) = initial_view { match self @@ -14537,7 +14548,6 @@ impl Session { }; self.refresh_terminal_catalogue().await; let terminal = self.native.as_mut().ok_or(())?; - terminal.launches.insert(terminal_handle, request.launch); if let Some(app_handle) = app_handle { terminal.app_handles.insert(terminal_handle, app_handle); } @@ -14659,11 +14669,14 @@ impl Session { return self.send_result(&frame, Status::NotFound, Vec::new()).await; }; let launch = match request.launch_mode { - yas_terminal::LaunchMode::Replay => self - .native - .as_ref() - .and_then(|terminal| terminal.launches.get(&request.terminal_handle)) - .cloned(), + yas_terminal::LaunchMode::Replay => { + let state = self.native.as_ref().ok_or(())?.state.clone(); + let session = state.session.lock().await; + session + .ptys + .get(&pty_id) + .and_then(|pty| pty.native_launch.clone()) + } yas_terminal::LaunchMode::Replace => request.launch.take(), }; let Some(mut launch) = launch else { @@ -14756,7 +14769,7 @@ impl Session { .send_sensitive_result(&frame, Status::NotFound, Vec::new()) .await; } - let (succeeded, invalidated_views) = { + let (failure, invalidated_views) = { let mut session = state.session.lock().await; let current = session.ptys.get(&pty_id).ok_or(())?; let (rows, cols) = current.driver.size(); @@ -14807,15 +14820,25 @@ impl Session { .ok_or(())?; debug_assert_eq!(lifecycle.backend_id(), pty_id); debug_assert_eq!(lifecycle.generation(), generation); - (true, true) + (None, true) } else { let invalidated = request.cutover_mode == yas_terminal::CutoverMode::StopThenStart; if invalidated { session.native_terminal_views.refresh_backend(pty_id); } - (false, invalidated) + let status = if super::pty::launch_program_missing( + prepared.spec.borrowed(&argv), + &state, + session_env.as_ref(), + ) { + Status::NotFound + } else { + Status::Io + }; + (Some(status), invalidated) } }; + let succeeded = failure.is_none(); let receipts = if invalidated_views { let terminal = self.native.as_mut().ok_or(())?; terminal @@ -14834,7 +14857,7 @@ impl Session { Vec::new() }; self.drain_terminal_frame_receipts(receipts).await?; - if !succeeded { + if let Some(status) = failure { if request.cutover_mode == yas_terminal::CutoverMode::StopThenStart && self.refresh_terminal_catalogue().await { @@ -14844,20 +14867,20 @@ impl Session { yas_wire::schema::terminal::request::RESTART, request.operation_id, fingerprint, - Status::Io, + status, Vec::new(), None, Some(request.terminal_handle), ); - return self - .send_sensitive_result(&frame, Status::Io, Vec::new()) - .await; + return self.send_sensitive_result(&frame, status, Vec::new()).await; + } + if request.launch_mode == yas_terminal::LaunchMode::Replace + && let Some(pty) = state.session.lock().await.ptys.get_mut(&pty_id) + { + pty.native_launch = Some(launch); } self.refresh_terminal_catalogue().await; let terminal = self.native.as_mut().ok_or(())?; - if request.launch_mode == yas_terminal::LaunchMode::Replace { - terminal.launches.insert(request.terminal_handle, launch); - } match app_handle { Some(app_handle) => { terminal @@ -14990,7 +15013,6 @@ impl Session { return self.send_result(&frame, Status::NotFound, Vec::new()).await; } if let Some(terminal) = self.native.as_mut() { - terminal.launches.remove(&request.terminal_handle); terminal.app_handles.remove(&request.terminal_handle); } if self.refresh_terminal_catalogue().await { @@ -27592,9 +27614,6 @@ impl Session { terminal .app_handles .retain(|handle, _| snapshot.records.contains_key(handle)); - terminal - .launches - .retain(|handle, _| snapshot.records.contains_key(handle)); // And the deduplication entries that named those terminals: the // replay guarantee runs to the end of the affected resource's // lifetime, which this is. @@ -53613,7 +53632,7 @@ mod tests { ) .await .status, - Status::Io, + Status::NotFound, ); { let shared = state.session.lock().await; @@ -53682,7 +53701,7 @@ mod tests { ) .await .status, - Status::Io, + Status::NotFound, ); { let shared = state.session.lock().await; @@ -53816,6 +53835,148 @@ mod tests { process_service.shutdown().await; } + #[cfg(unix)] + #[tokio::test(flavor = "multi_thread")] + async fn terminal_restart_replays_across_connections_and_missing_programs_are_not_found() { + let process_service = super::super::process::Server::new(false, true); + let state = super::super::tests::process_transport::test_state(process_service.clone()); + let (mut creator, creator_codec, _, creator_task) = + start_registered_session(state.clone(), &[family::TERMINAL]).await; + let mut creator_frames = Vec::new(); + write_request( + &mut creator, + &creator_codec, + family::TERMINAL, + yas_wire::schema::terminal::request::CREATE, + 10, + &yas_terminal::Create { + rows: 24, + cols: 80, + operation_id: [0x5a; 16], + launch: yas_terminal::Launch { + command: yas_terminal::Command::Argv(vec![ + b"/bin/sh".to_vec(), + b"-c".to_vec(), + b"exit 3".to_vec(), + ]), + cwd: yas_terminal::Cwd::ServerDefault, + environment_base: yas_terminal::EnvironmentBase::Server, + environment: Vec::new(), + extensions: Extensions::default(), + }, + extensions: Extensions::default(), + }, + ) + .await; + let created = next_terminal_result( + &mut creator, + &creator_codec, + yas_wire::schema::terminal::request::CREATE, + 10, + &mut creator_frames, + ) + .await; + assert_eq!(created.status, Status::Ok); + let created = yas_terminal::CreateResult::decode(&created.body).unwrap(); + drop(creator); + timeout(TEST_TIMEOUT, creator_task).await.unwrap().unwrap(); + + let (mut client, codec, _, task) = + start_registered_session(state.clone(), &[family::TERMINAL]).await; + let mut frames = Vec::new(); + write_request( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::RESTART, + 11, + &yas_terminal::Restart { + terminal_handle: created.terminal_handle, + operation_id: [0x5b; 16], + launch_mode: yas_terminal::LaunchMode::Replay, + cutover_mode: yas_terminal::CutoverMode::StopThenStart, + launch: None, + extensions: Extensions::default(), + }, + ) + .await; + let restarted = next_terminal_result( + &mut client, + &codec, + yas_wire::schema::terminal::request::RESTART, + 11, + &mut frames, + ) + .await; + assert_eq!(restarted.status, Status::Ok); + let restarted = yas_terminal::RestartResult::decode(&restarted.body).unwrap(); + assert!(restarted.generation > created.generation); + + write_request( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::CREATE, + 13, + &yas_terminal::Create { + rows: 24, + cols: 80, + operation_id: [0x5d; 16], + launch: yas_terminal::Launch { + command: yas_terminal::Command::Argv(vec![ + b"definitely-not-a-yas-terminal-program".to_vec(), + ]), + cwd: yas_terminal::Cwd::ServerDefault, + environment_base: yas_terminal::EnvironmentBase::Server, + environment: Vec::new(), + extensions: Extensions::default(), + }, + extensions: Extensions::default(), + }, + ) + .await; + assert_eq!( + next_terminal_result( + &mut client, + &codec, + yas_wire::schema::terminal::request::CREATE, + 13, + &mut frames, + ) + .await + .status, + Status::NotFound, + ); + + write_request( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::CLOSE, + 12, + &yas_terminal::Close { + terminal_handle: created.terminal_handle, + operation_id: [0x5c; 16], + }, + ) + .await; + assert_eq!( + next_terminal_result( + &mut client, + &codec, + yas_wire::schema::terminal::request::CLOSE, + 12, + &mut frames, + ) + .await + .status, + Status::Ok, + ); + drop(client); + timeout(TEST_TIMEOUT, task).await.unwrap().unwrap(); + process_service.shutdown().await; + } + /// A CREATE deduplication entry lasts exactly as long as the terminal it /// created. /// diff --git a/docs/design/yas.md b/docs/design/yas.md index f7ea86b00..0dac3132a 100644 --- a/docs/design/yas.md +++ b/docs/design/yas.md @@ -1564,6 +1564,9 @@ future REPLAY operations. REPLACE does not merge with the old command, cwd, environment, deadline, or application association. It can therefore restart a terminal with any argv or shell command, any valid cwd, and any environment the server OS can represent. REPLAY has no launch bytes; REPLACE requires them. +The stored launch record belongs to the terminal, not to the connection that +created it, so any client that can see the terminal can REPLAY it. A terminal +that was never started from a launch record answers REPLAY with `UNSUPPORTED`. RESTART is valid for RUNNING and EXITED terminals. For an EXITED terminal both cutover modes simply start the new generation. For a RUNNING terminal the diff --git a/docs/server.md b/docs/server.md index bc7173851..2a6280587 100644 --- a/docs/server.md +++ b/docs/server.md @@ -78,6 +78,10 @@ connections that are no longer needed. | `YAS_NOTIFICATION_TIMEOUT_MIN_MS` | `1000` | Lower clamp for positive application notification timeouts | | `YAS_NOTIFICATION_TIMEOUT_MAX_MS` | `86400000` | Upper clamp for positive application notification timeouts | +A terminal never inherits a `YAS_SOCK` from the server's own environment that +names a different server; `YAS_EXPORT_SOCK` replaces it with this server's +socket. + ### Named instances Every server has a name. `yas server` uses `default`; `yas server --name NAME` From e9997c72ea0b347367b3d787eae81ec7dabe31e1 Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 18:28:28 +0000 Subject: [PATCH 17/19] Print output of extension attempts that finish before ext run follows them follow_until_terminal returned as soon as a snapshot showed the definition stopped, skipping any attempt it had not followed yet. A short attempt that started and finished between two snapshots printed nothing. Follow every new attempt before honoring a terminal phase; FOLLOW replays retained output for a stopped attempt. Human output now also names a trapped or failed attempt and its detail on stderr, and a DEPLOY conflict on a name still held by another definition says which one and what to do, instead of 'Conflict: no detail'. --- crates/cli/src/yas_extension.rs | 188 +++++++++++++++++++++++++++----- 1 file changed, 163 insertions(+), 25 deletions(-) diff --git a/crates/cli/src/yas_extension.rs b/crates/cli/src/yas_extension.rs index 4c76ba8ee..11f377793 100644 --- a/crates/cli/src/yas_extension.rs +++ b/crates/cli/src/yas_extension.rs @@ -311,24 +311,44 @@ async fn run(client: &mut NativeClient, args: RunArgs) -> Result { if args.persist { flags |= schema::DEFINITION_PERSISTENT as u16; } - let identity = deploy( - client, - Deploy { - operation_id: operation_id(), - expected_extension_handle: 0, - expected_generation: 0, - expected_definition_revision: 0, - flags, - runtime: Runtime::Auto, - restart_policy: restart_policy(args.restart), - name: args.name, - content_hash: object.hash, - argv: argv.into_iter().map(String::into_bytes).collect(), - runtime_limits: default_runtime_limits(), - extensions: Extensions::default(), - }, - ) - .await?; + let name = args.name.clone(); + let request = Deploy { + operation_id: operation_id(), + expected_extension_handle: 0, + expected_generation: 0, + expected_definition_revision: 0, + flags, + runtime: Runtime::Auto, + restart_policy: restart_policy(args.restart), + name: args.name, + content_hash: object.hash, + argv: argv.into_iter().map(String::into_bytes).collect(), + runtime_limits: default_runtime_limits(), + extensions: Extensions::default(), + }; + let prefix = client + .request_result( + family::EXTENSION, + wire::request_kind::DEPLOY, + request.encode().map_err(wire_error)?, + true, + ) + .await?; + if prefix.status == yas_wire::core::Status::Conflict + && let Some(message) = name_conflict_message(&snapshot(client).await?, &name) + { + return Err(message); + } + if prefix.status != yas_wire::core::Status::Ok { + return Err(format!( + "YAS request {:#06x}/{:#06x} failed with {:?}: {}", + family::EXTENSION, + wire::request_kind::DEPLOY, + prefix.status, + crate::yas_native::format_result_detail(&prefix.detail) + )); + } + let identity = DefinitionIdentity::decode(&prefix.body).map_err(wire_error)?; let record = wait_for_identity(client, &identity).await?; if args.json { render_record(&record, true, "status"); @@ -485,6 +505,9 @@ async fn wait_until_started( } if matches!(current.phase, Phase::Stopped | Phase::Blocked) { render_record(¤t, json, "status"); + if !json && let Some(message) = exit_message(¤t) { + eprintln!("yas: {message}"); + } return Ok(exit_code(¤t).max(1)); } tokio::time::sleep(Duration::from_millis(200)).await; @@ -525,6 +548,15 @@ async fn follow_until_terminal( return Ok(130); } } + } else if matches!(current.phase, Phase::Stopped | Phase::Blocked) { + if json { + render_record(¤t, true, "status"); + } else if let Some(message) = exit_message(¤t) { + eprintln!("yas: {message}"); + } + return Ok(exit_code(¤t)); + } else { + tokio::time::sleep(Duration::from_millis(200)).await; } current = find_identity( &snapshot(client).await?, @@ -532,13 +564,6 @@ async fn follow_until_terminal( current.generation, )? .clone(); - if matches!(current.phase, Phase::Stopped | Phase::Blocked) { - if json { - render_record(¤t, true, "status"); - } - return Ok(exit_code(¤t)); - } - tokio::time::sleep(Duration::from_millis(200)).await; } } @@ -924,6 +949,58 @@ fn exit_code(record: &ExtensionRecord) -> i32 { } } +fn name_conflict_message(records: &[ExtensionRecord], name: &str) -> Option { + let existing = records.iter().find(|record| record.name == name)?; + let id = format_id(existing.extension_handle); + let phase = phase_name(existing.phase); + Some( + if existing.flags & schema::DEFINITION_PERSISTENT as u16 != 0 { + format!( + "a persistent extension named {name} already exists (id:{id}, phase {phase}); \ + use `yas ext update {name}` to replace it" + ) + } else if matches!(existing.phase, Phase::Stopped | Phase::Blocked) { + format!( + "a stopped transient extension named {name} (id:{id}) is still retained for output \ + replay; retry after it expires (YAS_EXT_TERMINAL_RETAIN, 30 s by default) or use \ + another name" + ) + } else { + format!( + "an extension named {name} is already {phase} (id:{id}); stop it with \ + `yas ext stop id:{id}` or use another name" + ) + }, + ) +} + +fn exit_message(record: &ExtensionRecord) -> Option { + let name = &record.name; + match record.last_exit.as_ref() { + Some(exit) if exit.kind == wire::ExitKind::Returned => None, + Some(exit) => { + let kind = match exit.kind { + wire::ExitKind::Returned => "returned", + wire::ExitKind::Trapped => "trapped", + wire::ExitKind::Cancelled => "was cancelled", + wire::ExitKind::Updated => "was replaced by an update", + wire::ExitKind::SlowConsumer => "was stopped as a slow consumer", + wire::ExitKind::ProtocolViolation => "violated the protocol", + wire::ExitKind::HostFailure => "failed in the host", + wire::ExitKind::ServerShutdown => "stopped for server shutdown", + wire::ExitKind::ResourceLimit => "exceeded a resource limit", + }; + if exit.detail.is_empty() { + Some(format!("extension {name} {kind}")) + } else { + Some(format!("extension {name} {kind}: {}", exit.detail)) + } + } + None if record.phase == Phase::Blocked => Some(format!("extension {name} is blocked")), + None => None, + } +} + fn phase_name(phase: Phase) -> &'static str { match phase { Phase::NeedObject => "need-object", @@ -1244,6 +1321,67 @@ fn loopback_url(url: &reqwest::Url) -> bool { mod tests { use super::*; + fn stopped(name: &str, flags: u16, last_exit: Option) -> ExtensionRecord { + ExtensionRecord { + extension_handle: 0x2a, + generation: 1, + definition_revision: 1, + phase: Phase::Stopped, + runtime: Runtime::QuickJs, + restart_policy: wire::RestartPolicy::Never, + flags, + attempt: 1, + last_running_attempt: 1, + task_id: 0, + next_start_unix_ms: 0, + directory_revision: 0, + content_hash: [0; 32], + name: name.into(), + last_exit, + runtime_limits: default_runtime_limits(), + extensions: Extensions::default(), + } + } + + fn exit(kind: wire::ExitKind, detail: &str) -> Option { + Some(wire::ExitRecord { + kind, + code: 0, + attempt: 1, + server_ns: 0, + detail: detail.into(), + extensions: Extensions::default(), + }) + } + + #[test] + fn a_trapped_attempt_names_its_error() { + let record = stopped( + "hello", + 0, + exit(wire::ExitKind::Trapped, "Error: boom at main.js:1"), + ); + assert_eq!( + exit_message(&record).as_deref(), + Some("extension hello trapped: Error: boom at main.js:1") + ); + assert_eq!(exit_code(&record), 1); + let returned = stopped("hello", 0, exit(wire::ExitKind::Returned, "")); + assert_eq!(exit_message(&returned), None); + } + + #[test] + fn a_name_conflict_explains_which_definition_holds_the_name() { + let transient = [stopped("hello", 0, exit(wire::ExitKind::Returned, ""))]; + let message = name_conflict_message(&transient, "hello").unwrap(); + assert!(message.contains("stopped transient"), "{message}"); + assert!(message.contains("000000000000002a"), "{message}"); + let persistent = [stopped("hello", schema::DEFINITION_PERSISTENT as u16, None)]; + let message = name_conflict_message(&persistent, "hello").unwrap(); + assert!(message.contains("yas ext update hello"), "{message}"); + assert_eq!(name_conflict_message(&transient, "other"), None); + } + #[test] fn selectors_never_infer_numeric_names() { assert!(matches!( From a527d518be57d65882cabc4b962f0eb9dfd56f97 Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 18:28:31 +0000 Subject: [PATCH 18/19] Document ext run output, trap, and name-retention behavior --- crates/cli/src/learn.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/cli/src/learn.md b/crates/cli/src/learn.md index 4612f7b56..dcb6254b7 100644 --- a/crates/cli/src/learn.md +++ b/crates/cli/src/learn.md @@ -378,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 From efdf9a0941c1a34170918a417870f4711f290e49 Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 18:28:28 +0000 Subject: [PATCH 19/19] Describe ext manage --from as a registry URL It only accepts https:// or loopback http:// URLs, never a directory. --- crates/cli/src/yas_extension_manage.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/yas_extension_manage.rs b/crates/cli/src/yas_extension_manage.rs index eda4f4094..e7b49e2f9 100644 --- a/crates/cli/src/yas_extension_manage.rs +++ b/crates/cli/src/yas_extension_manage.rs @@ -30,8 +30,9 @@ const MAX_MANIFEST_BYTES: u64 = 1024 * 1024; #[derive(Args, Clone, Debug)] pub(crate) struct ManageArgs { - /// Registry directory containing manifest.json and extension modules - #[arg(long, default_value = DEFAULT_REGISTRY)] + /// Registry base URL serving manifest.json and extension modules + /// (https://, or http:// on a loopback host) + #[arg(long, value_name = "URL", default_value = DEFAULT_REGISTRY)] pub from: String, }