authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-26 09:43:06-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-26 13:20:36-08:00
log8ab72e0964d95ca0ef139fba9994e3fb925343c9
tree991bb078087550e3672e51bb169462b412777db8
parent7ad5d03948a27e64bb11f6862d2f23333a0a2bd1

terminal nits


7 files changed, 144 insertions(+), 120 deletions(-)

apps/server/src/terminal/Layers/BunPTY.ts+4-4
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { Effect, Layer } from "effect";1import { Effect, Layer } from "effect";
2import { PtyAdapter, PtyExitEvent, PtyProcess, PtySpawnInput } from "../Services/PTY";2import { PtyAdapter, PtyAdapterShape, PtyExitEvent, PtyProcess } from "../Services/PTY";
33
4class BunPtyProcess implements PtyProcess {4class BunPtyProcess implements PtyProcess {
5 private readonly dataListeners = new Set<(data: string) => void>();5 private readonly dataListeners = new Set<(data: string) => void>();
...@@ -93,7 +93,7 @@ export const BunPtyAdapterLive = Layer.effect(...@@ -93,7 +93,7 @@ export const BunPtyAdapterLive = Layer.effect(
93 return yield* Effect.die("Bun PTY terminal support is unavailable on Windows.");93 return yield* Effect.die("Bun PTY terminal support is unavailable on Windows.");
94 }94 }
95 return {95 return {
96 spawn(input: PtySpawnInput): PtyProcess {96 spawn: (input) => {
97 let processHandle: BunPtyProcess | null = null;97 let processHandle: BunPtyProcess | null = null;
98 const command = [input.shell, ...(input.args ?? [])];98 const command = [input.shell, ...(input.args ?? [])];
99 const subprocess = Bun.spawn(command, {99 const subprocess = Bun.spawn(command, {
...@@ -108,8 +108,8 @@ export const BunPtyAdapterLive = Layer.effect(...@@ -108,8 +108,8 @@ export const BunPtyAdapterLive = Layer.effect(
108 },108 },
109 });109 });
110 processHandle = new BunPtyProcess(subprocess);110 processHandle = new BunPtyProcess(subprocess);
111 return processHandle;111 return Effect.succeed(processHandle);
112 },112 },
113 };113 } satisfies PtyAdapterShape;
114 }),114 }),
115);115);
apps/server/src/terminal/Layers/Manager.test.ts+3-3
...@@ -11,7 +11,7 @@ import { afterEach, describe, expect, it } from "vitest";...@@ -11,7 +11,7 @@ import { afterEach, describe, expect, it } from "vitest";
1111
12import type { PtyAdapterShape, PtyExitEvent, PtyProcess, PtySpawnInput } from "../Services/PTY";12import type { PtyAdapterShape, PtyExitEvent, PtyProcess, PtySpawnInput } from "../Services/PTY";
13import { TerminalManagerRuntime } from "./Manager";13import { TerminalManagerRuntime } from "./Manager";
14import { Encoding } from "effect";14import { Effect, Encoding } from "effect";
1515
16class FakePtyProcess implements PtyProcess {16class FakePtyProcess implements PtyProcess {
17 readonly writes: string[] = [];17 readonly writes: string[] = [];
...@@ -69,7 +69,7 @@ class FakePtyAdapter implements PtyAdapterShape {...@@ -69,7 +69,7 @@ class FakePtyAdapter implements PtyAdapterShape {
69 readonly spawnFailures: Error[] = [];69 readonly spawnFailures: Error[] = [];
70 private nextPid = 9000;70 private nextPid = 9000;
7171
72 spawn(input: PtySpawnInput): PtyProcess {72 spawn(input: PtySpawnInput): Effect.Effect<PtyProcess> {
73 this.spawnInputs.push(input);73 this.spawnInputs.push(input);
74 const failure = this.spawnFailures.shift();74 const failure = this.spawnFailures.shift();
75 if (failure) {75 if (failure) {
...@@ -77,7 +77,7 @@ class FakePtyAdapter implements PtyAdapterShape {...@@ -77,7 +77,7 @@ class FakePtyAdapter implements PtyAdapterShape {
77 }77 }
78 const process = new FakePtyProcess(this.nextPid++);78 const process = new FakePtyProcess(this.nextPid++);
79 this.processes.push(process);79 this.processes.push(process);
80 return process;80 return Effect.succeed(process);
81 }81 }
82}82}
8383
apps/server/src/terminal/Layers/Manager.ts+50-20
...@@ -20,6 +20,7 @@ import { runProcess } from "../../processRunner";...@@ -20,6 +20,7 @@ import { runProcess } from "../../processRunner";
20import { ServerConfig } from "../../config";20import { ServerConfig } from "../../config";
21import {21import {
22 ShellCandidate,22 ShellCandidate,
23 TerminalError,
23 TerminalManager,24 TerminalManager,
24 TerminalManagerShape,25 TerminalManagerShape,
25 TerminalSessionState,26 TerminalSessionState,
...@@ -552,14 +553,16 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>...@@ -552,14 +553,16 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
552553
553 for (const candidate of shellCandidates) {554 for (const candidate of shellCandidates) {
554 try {555 try {
555 ptyProcess = this.ptyAdapter.spawn({556 ptyProcess = Effect.runSync(
556 shell: candidate.shell,557 this.ptyAdapter.spawn({
557 ...(candidate.args ? { args: candidate.args } : {}),558 shell: candidate.shell,
558 cwd: session.cwd,559 ...(candidate.args ? { args: candidate.args } : {}),
559 cols: session.cols,560 cwd: session.cwd,
560 rows: session.rows,561 cols: session.cols,
561 env: terminalEnv,562 rows: session.rows,
562 });563 env: terminalEnv,
564 }),
565 );
563 startedShell = formatShellCandidate(candidate);566 startedShell = formatShellCandidate(candidate);
564 break;567 break;
565 } catch (error) {568 } catch (error) {
...@@ -690,7 +693,11 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>...@@ -690,7 +693,11 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
690 this.killEscalationTimers.delete(process);693 this.killEscalationTimers.delete(process);
691 }694 }
692695
693 private killProcessWithEscalation(process: PtyProcess, threadId: string, terminalId: string): void {696 private killProcessWithEscalation(
697 process: PtyProcess,
698 threadId: string,
699 terminalId: string,
700 ): void {
694 this.clearKillEscalationTimer(process);701 this.clearKillEscalationTimer(process);
695 try {702 try {
696 process.kill("SIGTERM");703 process.kill("SIGTERM");
...@@ -724,7 +731,9 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>...@@ -724,7 +731,9 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
724 }731 }
725732
726 private evictInactiveSessionsIfNeeded(): void {733 private evictInactiveSessionsIfNeeded(): void {
727 const inactiveSessions = [...this.sessions.values()].filter((session) => session.status !== "running");734 const inactiveSessions = [...this.sessions.values()].filter(
735 (session) => session.status !== "running",
736 );
728 if (inactiveSessions.length <= this.maxRetainedInactiveSessions) {737 if (inactiveSessions.length <= this.maxRetainedInactiveSessions) {
729 return;738 return;
730 }739 }
...@@ -1110,12 +1119,36 @@ export const TerminalManagerLive = Layer.effect(...@@ -1110,12 +1119,36 @@ export const TerminalManagerLive = Layer.effect(
1110 );1119 );
11111120
1112 return {1121 return {
1113 open: (input) => Effect.promise(() => runtime.open(input)),1122 open: (input) =>
1114 write: (input) => Effect.promise(() => runtime.write(input)),1123 Effect.tryPromise({
1115 resize: (input) => Effect.promise(() => runtime.resize(input)),1124 try: () => runtime.open(input),
1116 clear: (input) => Effect.promise(() => runtime.clear(input)),1125 catch: (cause) => new TerminalError({ message: "Failed to open terminal", cause }),
1117 restart: (input) => Effect.promise(() => runtime.restart(input)),1126 }),
1118 close: (input) => Effect.promise(() => runtime.close(input)),1127 write: (input) =>
1128 Effect.tryPromise({
1129 try: () => runtime.write(input),
1130 catch: (cause) => new TerminalError({ message: "Failed to write to terminal", cause }),
1131 }),
1132 resize: (input) =>
1133 Effect.tryPromise({
1134 try: () => runtime.resize(input),
1135 catch: (cause) => new TerminalError({ message: "Failed to resize terminal", cause }),
1136 }),
1137 clear: (input) =>
1138 Effect.tryPromise({
1139 try: () => runtime.clear(input),
1140 catch: (cause) => new TerminalError({ message: "Failed to clear terminal", cause }),
1141 }),
1142 restart: (input) =>
1143 Effect.tryPromise({
1144 try: () => runtime.restart(input),
1145 catch: (cause) => new TerminalError({ message: "Failed to restart terminal", cause }),
1146 }),
1147 close: (input) =>
1148 Effect.tryPromise({
1149 try: () => runtime.close(input),
1150 catch: (cause) => new TerminalError({ message: "Failed to close terminal", cause }),
1151 }),
1119 subscribe: (listener) =>1152 subscribe: (listener) =>
1120 Effect.sync(() => {1153 Effect.sync(() => {
1121 runtime.on("event", listener);1154 runtime.on("event", listener);
...@@ -1123,10 +1156,7 @@ export const TerminalManagerLive = Layer.effect(...@@ -1123,10 +1156,7 @@ export const TerminalManagerLive = Layer.effect(
1123 runtime.off("event", listener);1156 runtime.off("event", listener);
1124 };1157 };
1125 }),1158 }),
1126 dispose: () =>1159 dispose: Effect.sync(() => runtime.dispose()),
1127 Effect.sync(() => {
1128 runtime.dispose();
1129 }),
1130 } satisfies TerminalManagerShape;1160 } satisfies TerminalManagerShape;
1131 }),1161 }),
1132);1162);
apps/server/src/terminal/Layers/NodePTY.test.ts+24-44
...@@ -1,65 +1,45 @@...@@ -1,65 +1,45 @@
1import fs from "node:fs";1import { FileSystem, Path, Effect } from "effect";
2import os from "node:os";2import { assert, it } from "@effect/vitest";
3import path from "node:path";
4
5import { assert, it, vi } from "@effect/vitest";
6import { Effect, Layer } from "effect";
7import { afterEach } from "vitest";
83
9import { ensureNodePtySpawnHelperExecutable } from "./NodePTY";4import { ensureNodePtySpawnHelperExecutable } from "./NodePTY";
5import { NodeServices } from "@effect/platform-node";
106
11function fileMode(filePath: string): number {7it.layer(NodeServices.layer)("ensureNodePtySpawnHelperExecutable", (it) => {
12 return fs.statSync(filePath).mode & 0o777;
13}
14
15const tempDirs: string[] = [];
16
17afterEach(() => {
18 vi.unstubAllGlobals();
19 for (const dir of tempDirs.splice(0, tempDirs.length)) {
20 fs.rmSync(dir, { recursive: true, force: true });
21 }
22});
23
24const helperLayer = it.layer(Layer.empty);
25
26helperLayer("ensureNodePtySpawnHelperExecutable", (it) => {
27 it.effect("adds executable bits when helper exists but is not executable", () =>8 it.effect("adds executable bits when helper exists but is not executable", () =>
28 Effect.sync(() => {9 Effect.gen(function* () {
29 if (process.platform === "win32") return;10 if (process.platform === "win32") return;
3011
31 const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pty-helper-test-"));12 const fs = yield* FileSystem.FileSystem;
32 tempDirs.push(dir);13 const path = yield* Path.Path;
14
15 const dir = yield* fs.makeTempDirectoryScoped({ prefix: "pty-helper-test-" });
33 const helperPath = path.join(dir, "spawn-helper");16 const helperPath = path.join(dir, "spawn-helper");
34 fs.writeFileSync(helperPath, "#!/bin/sh\nexit 0\n", "utf8");17 yield* fs.writeFileString(helperPath, "#!/bin/sh\nexit 0\n");
35 fs.chmodSync(helperPath, 0o644);18 yield* fs.chmod(helperPath, 0o644);
3619
37 ensureNodePtySpawnHelperExecutable(helperPath);20 yield* ensureNodePtySpawnHelperExecutable(helperPath);
3821
39 assert.equal(fileMode(helperPath) & 0o111, 0o111);22 const mode = (yield* fs.stat(helperPath)).mode & 0o777;
23 assert.equal(mode & 0o111, 0o111);
40 }),24 }),
41 );25 );
4226
43 it.effect("keeps executable helper as executable", () =>27 it.effect("keeps executable helper as executable", () =>
44 Effect.sync(() => {28 Effect.gen(function* () {
45 if (process.platform === "win32") return;29 if (process.platform === "win32") return;
4630
47 const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pty-helper-test-"));31 const fs = yield* FileSystem.FileSystem;
48 tempDirs.push(dir);32 const path = yield* Path.Path;
49 const helperPath = path.join(dir, "spawn-helper");
50 fs.writeFileSync(helperPath, "#!/bin/sh\nexit 0\n", "utf8");
51 fs.chmodSync(helperPath, 0o755);
5233
53 ensureNodePtySpawnHelperExecutable(helperPath);34 const dir = yield* fs.makeTempDirectoryScoped({ prefix: "pty-helper-test-" });
35 const helperPath = path.join(dir, "spawn-helper");
36 yield* fs.writeFileString(helperPath, "#!/bin/sh\nexit 0\n");
37 yield* fs.chmod(helperPath, 0o755);
5438
55 assert.equal(fileMode(helperPath) & 0o111, 0o111);39 yield* ensureNodePtySpawnHelperExecutable(helperPath);
56 }),
57 );
5840
59 it.effect("does not throw when helper path is missing", () =>41 const mode = (yield* fs.stat(helperPath)).mode & 0o777;
60 Effect.sync(() => {42 assert.equal(mode & 0o111, 0o111);
61 const missing = path.join(os.tmpdir(), `does-not-exist-${Date.now()}`);
62 ensureNodePtySpawnHelperExecutable(missing);
63 }),43 }),
64 );44 );
65});45});
apps/server/src/terminal/Layers/NodePTY.ts+44-39
...@@ -1,55 +1,48 @@...@@ -1,55 +1,48 @@
1import fs from "node:fs";
2import { createRequire } from "node:module";1import { createRequire } from "node:module";
3import path from "node:path";
42
5import { Effect, Layer } from "effect";3import { Effect, FileSystem, Layer, Path } from "effect";
6import { PtyAdapter, PtyExitEvent, PtyProcess, PtySpawnInput } from "../Services/PTY";4import { PtyAdapter, PtyAdapterShape, PtyExitEvent, PtyProcess } from "../Services/PTY";
75
8const requireForNodePty = createRequire(import.meta.url);
9let didEnsureSpawnHelperExecutable = false;6let didEnsureSpawnHelperExecutable = false;
107
11function resolveNodePtySpawnHelperPath(): string | null {8const resolveNodePtySpawnHelperPath = Effect.gen(function* () {
12 try {9 const requireForNodePty = createRequire(import.meta.url);
13 const packageJsonPath = requireForNodePty.resolve("node-pty/package.json");10 const path = yield* Path.Path;
14 const packageDir = path.dirname(packageJsonPath);11 const fs = yield* FileSystem.FileSystem;
15 const candidates = [12
16 path.join(packageDir, "build", "Release", "spawn-helper"),13 const packageJsonPath = requireForNodePty.resolve("node-pty/package.json");
17 path.join(packageDir, "build", "Debug", "spawn-helper"),14 const packageDir = path.dirname(packageJsonPath);
18 path.join(packageDir, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),15 const candidates = [
19 ];16 path.join(packageDir, "build", "Release", "spawn-helper"),
2017 path.join(packageDir, "build", "Debug", "spawn-helper"),
21 for (const candidate of candidates) {18 path.join(packageDir, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
22 if (fs.existsSync(candidate)) {19 ];
23 return candidate;20
24 }21 for (const candidate of candidates) {
22 if (yield* fs.exists(candidate)) {
23 return candidate;
25 }24 }
26
27 return null;
28 } catch {
29 return null;
30 }25 }
31}26 return null;
27}).pipe(Effect.orElseSucceed(() => null));
3228
33export function ensureNodePtySpawnHelperExecutable(explicitPath?: string): void {29export const ensureNodePtySpawnHelperExecutable = Effect.fn(function* (explicitPath?: string) {
30 const fs = yield* FileSystem.FileSystem;
34 if (process.platform === "win32") return;31 if (process.platform === "win32") return;
35 if (!explicitPath && didEnsureSpawnHelperExecutable) return;32 if (!explicitPath && didEnsureSpawnHelperExecutable) return;
3633
37 const helperPath = explicitPath ?? resolveNodePtySpawnHelperPath();34 const helperPath = explicitPath ?? (yield* resolveNodePtySpawnHelperPath);
38 if (!helperPath) return;35 if (!helperPath) return;
39 if (!explicitPath) {36 if (!explicitPath) {
40 didEnsureSpawnHelperExecutable = true;37 didEnsureSpawnHelperExecutable = true;
41 }38 }
4239
43 try {40 const stat = yield* fs.stat(helperPath);
44 const stat = fs.statSync(helperPath);41 const mode = stat.mode & 0o777;
45 const mode = stat.mode & 0o777;42 if ((mode & 0o111) === 0) {
46 if ((mode & 0o111) === 0) {43 yield* fs.chmod(helperPath, mode | 0o111);
47 fs.chmodSync(helperPath, mode | 0o111);
48 }
49 } catch {
50 // Best effort only. If chmod fails, node-pty spawn will surface the real error.
51 }44 }
52}45});
5346
54class NodePtyProcess implements PtyProcess {47class NodePtyProcess implements PtyProcess {
55 constructor(private readonly process: import("node-pty").IPty) {}48 constructor(private readonly process: import("node-pty").IPty) {}
...@@ -93,10 +86,22 @@ class NodePtyProcess implements PtyProcess {...@@ -93,10 +86,22 @@ class NodePtyProcess implements PtyProcess {
93export const NodePtyAdapterLive = Layer.effect(86export const NodePtyAdapterLive = Layer.effect(
94 PtyAdapter,87 PtyAdapter,
95 Effect.gen(function* () {88 Effect.gen(function* () {
89 const fs = yield* FileSystem.FileSystem;
90 const path = yield* Path.Path;
91
96 const nodePty = yield* Effect.promise(() => import("node-pty"));92 const nodePty = yield* Effect.promise(() => import("node-pty"));
93
94 const ensureNodePtySpawnHelperExecutableCached = yield* Effect.cached(
95 ensureNodePtySpawnHelperExecutable().pipe(
96 Effect.provideService(FileSystem.FileSystem, fs),
97 Effect.provideService(Path.Path, path),
98 Effect.orElseSucceed(() => undefined),
99 ),
100 );
101
97 return {102 return {
98 spawn(input: PtySpawnInput): PtyProcess {103 spawn: Effect.fn(function* (input) {
99 ensureNodePtySpawnHelperExecutable();104 yield* ensureNodePtySpawnHelperExecutableCached;
100 const ptyProcess = nodePty.spawn(input.shell, input.args ?? [], {105 const ptyProcess = nodePty.spawn(input.shell, input.args ?? [], {
101 cwd: input.cwd,106 cwd: input.cwd,
102 cols: input.cols,107 cols: input.cols,
...@@ -105,7 +110,7 @@ export const NodePtyAdapterLive = Layer.effect(...@@ -105,7 +110,7 @@ export const NodePtyAdapterLive = Layer.effect(
105 name: globalThis.process.platform === "win32" ? "xterm-color" : "xterm-256color",110 name: globalThis.process.platform === "win32" ? "xterm-color" : "xterm-256color",
106 });111 });
107 return new NodePtyProcess(ptyProcess);112 return new NodePtyProcess(ptyProcess);
108 },113 }),
109 };114 } satisfies PtyAdapterShape;
110 }),115 }),
111);116);
apps/server/src/terminal/Services/Manager.ts+17-8
...@@ -17,7 +17,12 @@ import {...@@ -17,7 +17,12 @@ import {
17 TerminalWriteInput,17 TerminalWriteInput,
18} from "@t3tools/contracts";18} from "@t3tools/contracts";
19import { PtyProcess } from "./PTY";19import { PtyProcess } from "./PTY";
20import { Effect, ServiceMap } from "effect";20import { Effect, Schema, ServiceMap } from "effect";
21
22export class TerminalError extends Schema.TaggedErrorClass<TerminalError>()("TerminalError", {
23 message: Schema.String,
24 cause: Schema.optional(Schema.Defect),
25}) {}
2126
22export interface TerminalSessionState {27export interface TerminalSessionState {
23 threadId: string;28 threadId: string;
...@@ -58,36 +63,40 @@ export interface TerminalManagerShape {...@@ -58,36 +63,40 @@ export interface TerminalManagerShape {
58 * Reuses an existing session for the same thread/terminal id and restores63 * Reuses an existing session for the same thread/terminal id and restores
59 * persisted history on first open.64 * persisted history on first open.
60 */65 */
61 readonly open: (input: TerminalOpenInput) => Effect.Effect<TerminalSessionSnapshot, unknown>;66 readonly open: (
67 input: TerminalOpenInput,
68 ) => Effect.Effect<TerminalSessionSnapshot, TerminalError>;
6269
63 /**70 /**
64 * Write input bytes to a terminal session.71 * Write input bytes to a terminal session.
65 */72 */
66 readonly write: (input: TerminalWriteInput) => Effect.Effect<void, unknown>;73 readonly write: (input: TerminalWriteInput) => Effect.Effect<void, TerminalError>;
6774
68 /**75 /**
69 * Resize the PTY backing a terminal session.76 * Resize the PTY backing a terminal session.
70 */77 */
71 readonly resize: (input: TerminalResizeInput) => Effect.Effect<void, unknown>;78 readonly resize: (input: TerminalResizeInput) => Effect.Effect<void, TerminalError>;
7279
73 /**80 /**
74 * Clear terminal output history.81 * Clear terminal output history.
75 */82 */
76 readonly clear: (input: TerminalClearInput) => Effect.Effect<void, unknown>;83 readonly clear: (input: TerminalClearInput) => Effect.Effect<void, TerminalError>;
7784
78 /**85 /**
79 * Restart a terminal session in place.86 * Restart a terminal session in place.
80 *87 *
81 * Always resets history before spawning the new process.88 * Always resets history before spawning the new process.
82 */89 */
83 readonly restart: (input: TerminalOpenInput) => Effect.Effect<TerminalSessionSnapshot, unknown>;90 readonly restart: (
91 input: TerminalOpenInput,
92 ) => Effect.Effect<TerminalSessionSnapshot, TerminalError>;
8493
85 /**94 /**
86 * Close an active terminal session.95 * Close an active terminal session.
87 *96 *
88 * When `terminalId` is omitted, closes all sessions for the thread.97 * When `terminalId` is omitted, closes all sessions for the thread.
89 */98 */
90 readonly close: (input: TerminalCloseInput) => Effect.Effect<void, unknown>;99 readonly close: (input: TerminalCloseInput) => Effect.Effect<void, TerminalError>;
91100
92 /**101 /**
93 * Subscribe to terminal runtime events.102 * Subscribe to terminal runtime events.
...@@ -97,7 +106,7 @@ export interface TerminalManagerShape {...@@ -97,7 +106,7 @@ export interface TerminalManagerShape {
97 /**106 /**
98 * Dispose all managed terminal resources.107 * Dispose all managed terminal resources.
99 */108 */
100 readonly dispose: () => Effect.Effect<void>;109 readonly dispose: Effect.Effect<void>;
101}110}
102111
103/**112/**
apps/server/src/terminal/Services/PTY.ts+2-2
...@@ -6,7 +6,7 @@...@@ -6,7 +6,7 @@
6 *6 *
7 * @module PtyAdapter7 * @module PtyAdapter
8 */8 */
9import { ServiceMap } from "effect";9import { Effect, ServiceMap } from "effect";
1010
11export interface PtyExitEvent {11export interface PtyExitEvent {
12 exitCode: number;12 exitCode: number;
...@@ -38,7 +38,7 @@ export interface PtyAdapterShape {...@@ -38,7 +38,7 @@ export interface PtyAdapterShape {
38 /**38 /**
39 * Spawn a PTY process for a terminal session.39 * Spawn a PTY process for a terminal session.
40 */40 */
41 spawn(input: PtySpawnInput): PtyProcess;41 spawn(input: PtySpawnInput): Effect.Effect<PtyProcess>;
42}42}
4343
44/**44/**