···4455### Client API
66- Add `env?: Record<string, string>` to `SpawnDaemonOptions` and `ServerOptions` — use this to spawn a session child with a verbatim environment (no inheritance from the daemon's `process.env`, no allow-list). `PTY_SESSION` is always injected on top so nesting detection and `pty exec` keep working. Mutually exclusive with `isolateEnv` / `extraEnv` (throws at daemon startup if both are passed). Requested by the pty-layout dev to spawn a launcher shell with a shim `tmux` on `PATH`, a custom `TMUX` marker, and a filter-tag env var.
77+- Add `PtyHandle.readWrappedFlags(scrollOffset?): boolean[]` — per-row flags aligned with `readCells`, where `true` means the row continues the previous row because xterm wrapped a long line (not because the child emitted `\n`). Intended for consumers reconstructing logical lines from a visually-multi-row text selection (e.g., copying a wrapped URL to the clipboard without a spurious newline). Passes through xterm.js's `IBufferLine.isWrapped`; works for both `createPty` embedded terminals and `attachPty` remote sessions since the serialize/replay path preserves wrap state.
7889## 0.9.0
910
+30
src/tui/builders.ts
···366366 return grid;
367367}
368368369369+/** Read per-row `isWrapped` flags aligned with `readXtermCells(scrollOffset)`.
370370+ * A `true` at index `r` means that row continues the previous row because
371371+ * xterm wrapped a long line (not because the child emitted `\n`). Used by
372372+ * consumers reconstructing logical lines from a visually-multi-row
373373+ * selection — e.g., copying a wrapped URL without a spurious newline. */
374374+function readXtermWrappedFlags(
375375+ terminal: any,
376376+ rows: number,
377377+ scrollOffset: number = 0,
378378+): boolean[] {
379379+ const buf = terminal.buffer.active;
380380+ const baseY = buf.baseY as number;
381381+ const startLine = Math.max(0, baseY - scrollOffset);
382382+ const flags: boolean[] = new Array(rows);
383383+ for (let r = 0; r < rows; r++) {
384384+ const lineIdx = startLine + r;
385385+ const line = lineIdx < buf.length ? buf.getLine(lineIdx) : null;
386386+ flags[r] = !!(line?.isWrapped);
387387+ }
388388+ return flags;
389389+}
390390+369391export function createPty(
370392 command: string,
371393 args: string[] = [],
···471493472494 readCells(scrollOffset?: number) {
473495 return readXtermCells(terminal, handle.rows, handle.cols, scrollOffset ?? 0);
496496+ },
497497+498498+ readWrappedFlags(scrollOffset?: number) {
499499+ return readXtermWrappedFlags(terminal, handle.rows, scrollOffset ?? 0);
474500 },
475501476502 kill() {
···607633608634 readCells(scrollOffset?: number) {
609635 return readXtermCells(terminal, handle.rows, handle.cols, scrollOffset ?? 0);
636636+ },
637637+638638+ readWrappedFlags(scrollOffset?: number) {
639639+ return readXtermWrappedFlags(terminal, handle.rows, scrollOffset ?? 0);
610640 },
611641612642 kill() {
+13
src/tui/nodes.ts
···239239 * @param scrollOffset Lines to scroll back into history (0 = live viewport).
240240 */
241241 readCells(scrollOffset?: number): { char: string; fg: [number, number, number] | null; bg: [number, number, number] | null; bold: boolean; dim: boolean; italic: boolean; underline: boolean }[][];
242242+ /**
243243+ * Read per-row "wrapped" flags aligned with the rows returned by
244244+ * `readCells(scrollOffset)`. A `true` at index `r` means row `r`
245245+ * continues from row `r-1` because the terminal wrapped a long line
246246+ * rather than the child emitting a real newline — the same signal
247247+ * xterm.js exposes via `IBufferLine.isWrapped`.
248248+ *
249249+ * Intended use: reconstructing logical lines from a multi-row text
250250+ * selection (e.g., copying a wrapped URL to the clipboard without a
251251+ * spurious `\n` in the middle).
252252+ * @param scrollOffset Lines to scroll back into history (0 = live viewport).
253253+ */
254254+ readWrappedFlags(scrollOffset?: number): boolean[];
242255 /** Current PTY dimensions. */
243256 cols: number;
244257 rows: number;
+5-3
tests/integration.test.ts
···10491049 }
10501050 });
1051105110521052- // Send items with 200ms delay between each
10531053- const { send } = await import("../src/client.ts");
10541054- send({ name, data: ["A", "B", "C"], delayMs: 200 });
10521052+ // Send items with 200ms delay between each. Use the programmatic
10531053+ // `sendData` (Promise-returning, no process.exit) — the CLI `send`
10541054+ // shape is for binaries, not tests.
10551055+ const { sendData } = await import("../src/connection.ts");
10561056+ void sendData({ name, data: ["A", "B", "C"], delayMs: 200 });
1055105710561058 // Wait for all items to arrive
10571059 await new Promise((r) => setTimeout(r, 1000));
+55
tests/pty-handle.test.ts
···177177 });
178178});
179179180180+// --- readWrappedFlags ---
181181+182182+describe("readWrappedFlags", () => {
183183+ it("returns one flag per visible row", () => {
184184+ const h = spawn("cat", [], { rows: 10, cols: 40 });
185185+ const flags = h.readWrappedFlags();
186186+ expect(flags).toHaveLength(10);
187187+ expect(flags.every((f) => typeof f === "boolean")).toBe(true);
188188+ });
189189+190190+ it("marks continuation rows as wrapped when a long line overflows", async () => {
191191+ // 120-char line at 40-col terminal → wraps to 3 rows. First row is
192192+ // not wrapped (it's the start of the line); the next two rows are
193193+ // continuations and should both be flagged.
194194+ const h = spawn("bash", ["-c", "printf '%0.sa' {1..120}; sleep 5"], {
195195+ rows: 12, cols: 40,
196196+ });
197197+ await waitFor(h, () => h.readWrappedFlags().some((f) => f === true));
198198+ const flags = h.readWrappedFlags();
199199+ // First row: start of the logical line, not a wrap continuation.
200200+ expect(flags[0]).toBe(false);
201201+ // Next two rows: wrapped continuations.
202202+ expect(flags[1]).toBe(true);
203203+ expect(flags[2]).toBe(true);
204204+ // Remaining empty rows after the wrapped content: not wrapped.
205205+ expect(flags[3]).toBe(false);
206206+ });
207207+208208+ it("short lines followed by a newline produce no wrapped flags", async () => {
209209+ const h = spawn("bash", ["-c", "printf 'short\\n'; sleep 5"], {
210210+ rows: 8, cols: 40,
211211+ });
212212+ await waitFor(h, () => h.cursorRow >= 1);
213213+ const flags = h.readWrappedFlags();
214214+ expect(flags.every((f) => f === false)).toBe(true);
215215+ });
216216+217217+ it("scrollOffset shifts the window in sync with readCells", async () => {
218218+ // Emit many short lines to push earlier content into scrollback,
219219+ // then read with a nonzero offset and confirm flags array length
220220+ // still matches readCells rows.
221221+ const h = spawn("bash", ["-c", "for i in $(seq 1 30); do echo line $i; done; sleep 5"], {
222222+ rows: 10, cols: 40, scrollback: 100,
223223+ });
224224+ await waitFor(h, () => h.bufferLength > h.rows);
225225+ const flags0 = h.readWrappedFlags(0);
226226+ const cells0 = h.readCells(0);
227227+ expect(flags0.length).toBe(cells0.length);
228228+229229+ const flags5 = h.readWrappedFlags(5);
230230+ const cells5 = h.readCells(5);
231231+ expect(flags5.length).toBe(cells5.length);
232232+ });
233233+});
234234+180235// --- scrollback ---
181236182237describe("scrollback", () => {