| author | |
| committer | |
| log | 8ab72e0964d95ca0ef139fba9994e3fb925343c9 |
| tree | 991bb078087550e3672e51bb169462b412777db8 |
| parent | 7ad5d03948a27e64bb11f6862d2f23333a0a2bd1 |
7 files changed, 144 insertions(+), 120 deletions(-)
apps/server/src/terminal/Layers/BunPTY.ts+4-4| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | import { Effect, Layer } from "effect"; |
| 2 | import { PtyAdapter, PtyExitEvent, PtyProcess, PtySpawnInput } from "../Services/PTY"; | |
| 2 | import { PtyAdapter, PtyAdapterShape, PtyExitEvent, PtyProcess } from "../Services/PTY"; | |
| 3 | 3 | |
| 4 | 4 | class BunPtyProcess implements PtyProcess { |
| 5 | 5 | private readonly dataListeners = new Set<(data: string) => void>(); |
| ... | ... | @@ -93,7 +93,7 @@ export const BunPtyAdapterLive = Layer.effect( |
| 93 | 93 | return yield* Effect.die("Bun PTY terminal support is unavailable on Windows."); |
| 94 | 94 | } |
| 95 | 95 | return { |
| 96 | spawn(input: PtySpawnInput): PtyProcess { | |
| 96 | spawn: (input) => { | |
| 97 | 97 | let processHandle: BunPtyProcess | null = null; |
| 98 | 98 | const command = [input.shell, ...(input.args ?? [])]; |
| 99 | 99 | const subprocess = Bun.spawn(command, { |
| ... | ... | @@ -108,8 +108,8 @@ export const BunPtyAdapterLive = Layer.effect( |
| 108 | 108 | }, |
| 109 | 109 | }); |
| 110 | 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 | 11 | |
| 12 | 12 | import type { PtyAdapterShape, PtyExitEvent, PtyProcess, PtySpawnInput } from "../Services/PTY"; |
| 13 | 13 | import { TerminalManagerRuntime } from "./Manager"; |
| 14 | import { Encoding } from "effect"; | |
| 14 | import { Effect, Encoding } from "effect"; | |
| 15 | 15 | |
| 16 | 16 | class FakePtyProcess implements PtyProcess { |
| 17 | 17 | readonly writes: string[] = []; |
| ... | ... | @@ -69,7 +69,7 @@ class FakePtyAdapter implements PtyAdapterShape { |
| 69 | 69 | readonly spawnFailures: Error[] = []; |
| 70 | 70 | private nextPid = 9000; |
| 71 | 71 | |
| 72 | spawn(input: PtySpawnInput): PtyProcess { | |
| 72 | spawn(input: PtySpawnInput): Effect.Effect<PtyProcess> { | |
| 73 | 73 | this.spawnInputs.push(input); |
| 74 | 74 | const failure = this.spawnFailures.shift(); |
| 75 | 75 | if (failure) { |
| ... | ... | @@ -77,7 +77,7 @@ class FakePtyAdapter implements PtyAdapterShape { |
| 77 | 77 | } |
| 78 | 78 | const process = new FakePtyProcess(this.nextPid++); |
| 79 | 79 | this.processes.push(process); |
| 80 | return process; | |
| 80 | return Effect.succeed(process); | |
| 81 | 81 | } |
| 82 | 82 | } |
| 83 | 83 |
apps/server/src/terminal/Layers/Manager.ts+50-20| ... | ... | @@ -20,6 +20,7 @@ import { runProcess } from "../../processRunner"; |
| 20 | 20 | import { ServerConfig } from "../../config"; |
| 21 | 21 | import { |
| 22 | 22 | ShellCandidate, |
| 23 | TerminalError, | |
| 23 | 24 | TerminalManager, |
| 24 | 25 | TerminalManagerShape, |
| 25 | 26 | TerminalSessionState, |
| ... | ... | @@ -552,14 +553,16 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents> |
| 552 | 553 | |
| 553 | 554 | for (const candidate of shellCandidates) { |
| 554 | 555 | try { |
| 555 | ptyProcess = this.ptyAdapter.spawn({ | |
| 556 | shell: candidate.shell, | |
| 557 | ...(candidate.args ? { args: candidate.args } : {}), | |
| 558 | cwd: session.cwd, | |
| 559 | cols: session.cols, | |
| 560 | rows: session.rows, | |
| 561 | env: terminalEnv, | |
| 562 | }); | |
| 556 | ptyProcess = Effect.runSync( | |
| 557 | this.ptyAdapter.spawn({ | |
| 558 | shell: candidate.shell, | |
| 559 | ...(candidate.args ? { args: candidate.args } : {}), | |
| 560 | cwd: session.cwd, | |
| 561 | cols: session.cols, | |
| 562 | rows: session.rows, | |
| 563 | env: terminalEnv, | |
| 564 | }), | |
| 565 | ); | |
| 563 | 566 | startedShell = formatShellCandidate(candidate); |
| 564 | 567 | break; |
| 565 | 568 | } catch (error) { |
| ... | ... | @@ -690,7 +693,11 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents> |
| 690 | 693 | this.killEscalationTimers.delete(process); |
| 691 | 694 | } |
| 692 | 695 | |
| 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 | 701 | this.clearKillEscalationTimer(process); |
| 695 | 702 | try { |
| 696 | 703 | process.kill("SIGTERM"); |
| ... | ... | @@ -724,7 +731,9 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents> |
| 724 | 731 | } |
| 725 | 732 | |
| 726 | 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 | 737 | if (inactiveSessions.length <= this.maxRetainedInactiveSessions) { |
| 729 | 738 | return; |
| 730 | 739 | } |
| ... | ... | @@ -1110,12 +1119,36 @@ export const TerminalManagerLive = Layer.effect( |
| 1110 | 1119 | ); |
| 1111 | 1120 | |
| 1112 | 1121 | return { |
| 1113 | open: (input) => Effect.promise(() => runtime.open(input)), | |
| 1114 | write: (input) => Effect.promise(() => runtime.write(input)), | |
| 1115 | resize: (input) => Effect.promise(() => runtime.resize(input)), | |
| 1116 | clear: (input) => Effect.promise(() => runtime.clear(input)), | |
| 1117 | restart: (input) => Effect.promise(() => runtime.restart(input)), | |
| 1118 | close: (input) => Effect.promise(() => runtime.close(input)), | |
| 1122 | open: (input) => | |
| 1123 | Effect.tryPromise({ | |
| 1124 | try: () => runtime.open(input), | |
| 1125 | catch: (cause) => new TerminalError({ message: "Failed to open terminal", cause }), | |
| 1126 | }), | |
| 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 | 1152 | subscribe: (listener) => |
| 1120 | 1153 | Effect.sync(() => { |
| 1121 | 1154 | runtime.on("event", listener); |
| ... | ... | @@ -1123,10 +1156,7 @@ export const TerminalManagerLive = Layer.effect( |
| 1123 | 1156 | runtime.off("event", listener); |
| 1124 | 1157 | }; |
| 1125 | 1158 | }), |
| 1126 | dispose: () => | |
| 1127 | Effect.sync(() => { | |
| 1128 | runtime.dispose(); | |
| 1129 | }), | |
| 1159 | dispose: Effect.sync(() => runtime.dispose()), | |
| 1130 | 1160 | } satisfies TerminalManagerShape; |
| 1131 | 1161 | }), |
| 1132 | 1162 | ); |
apps/server/src/terminal/Layers/NodePTY.test.ts+24-44| ... | ... | @@ -1,65 +1,45 @@ |
| 1 | import fs from "node:fs"; | |
| 2 | import os from "node:os"; | |
| 3 | import path from "node:path"; | |
| 4 | ||
| 5 | import { assert, it, vi } from "@effect/vitest"; | |
| 6 | import { Effect, Layer } from "effect"; | |
| 7 | import { afterEach } from "vitest"; | |
| 1 | import { FileSystem, Path, Effect } from "effect"; | |
| 2 | import { assert, it } from "@effect/vitest"; | |
| 8 | 3 | |
| 9 | 4 | import { ensureNodePtySpawnHelperExecutable } from "./NodePTY"; |
| 5 | import { NodeServices } from "@effect/platform-node"; | |
| 10 | 6 | |
| 11 | function fileMode(filePath: string): number { | |
| 12 | return fs.statSync(filePath).mode & 0o777; | |
| 13 | } | |
| 14 | ||
| 15 | const tempDirs: string[] = []; | |
| 16 | ||
| 17 | afterEach(() => { | |
| 18 | vi.unstubAllGlobals(); | |
| 19 | for (const dir of tempDirs.splice(0, tempDirs.length)) { | |
| 20 | fs.rmSync(dir, { recursive: true, force: true }); | |
| 21 | } | |
| 22 | }); | |
| 23 | ||
| 24 | const helperLayer = it.layer(Layer.empty); | |
| 25 | ||
| 26 | helperLayer("ensureNodePtySpawnHelperExecutable", (it) => { | |
| 7 | it.layer(NodeServices.layer)("ensureNodePtySpawnHelperExecutable", (it) => { | |
| 27 | 8 | it.effect("adds executable bits when helper exists but is not executable", () => |
| 28 | Effect.sync(() => { | |
| 9 | Effect.gen(function* () { | |
| 29 | 10 | if (process.platform === "win32") return; |
| 30 | 11 | |
| 31 | const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pty-helper-test-")); | |
| 32 | tempDirs.push(dir); | |
| 12 | const fs = yield* FileSystem.FileSystem; | |
| 13 | const path = yield* Path.Path; | |
| 14 | ||
| 15 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "pty-helper-test-" }); | |
| 33 | 16 | const helperPath = path.join(dir, "spawn-helper"); |
| 34 | fs.writeFileSync(helperPath, "#!/bin/sh\nexit 0\n", "utf8"); | |
| 35 | fs.chmodSync(helperPath, 0o644); | |
| 17 | yield* fs.writeFileString(helperPath, "#!/bin/sh\nexit 0\n"); | |
| 18 | yield* fs.chmod(helperPath, 0o644); | |
| 36 | 19 | |
| 37 | ensureNodePtySpawnHelperExecutable(helperPath); | |
| 20 | yield* ensureNodePtySpawnHelperExecutable(helperPath); | |
| 38 | 21 | |
| 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 | ); |
| 42 | 26 | |
| 43 | 27 | it.effect("keeps executable helper as executable", () => |
| 44 | Effect.sync(() => { | |
| 28 | Effect.gen(function* () { | |
| 45 | 29 | if (process.platform === "win32") return; |
| 46 | 30 | |
| 47 | const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pty-helper-test-")); | |
| 48 | tempDirs.push(dir); | |
| 49 | const helperPath = path.join(dir, "spawn-helper"); | |
| 50 | fs.writeFileSync(helperPath, "#!/bin/sh\nexit 0\n", "utf8"); | |
| 51 | fs.chmodSync(helperPath, 0o755); | |
| 31 | const fs = yield* FileSystem.FileSystem; | |
| 32 | const path = yield* Path.Path; | |
| 52 | 33 | |
| 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); | |
| 54 | 38 | |
| 55 | assert.equal(fileMode(helperPath) & 0o111, 0o111); | |
| 56 | }), | |
| 57 | ); | |
| 39 | yield* ensureNodePtySpawnHelperExecutable(helperPath); | |
| 58 | 40 | |
| 59 | it.effect("does not throw when helper path is missing", () => | |
| 60 | Effect.sync(() => { | |
| 61 | const missing = path.join(os.tmpdir(), `does-not-exist-${Date.now()}`); | |
| 62 | ensureNodePtySpawnHelperExecutable(missing); | |
| 41 | const mode = (yield* fs.stat(helperPath)).mode & 0o777; | |
| 42 | assert.equal(mode & 0o111, 0o111); | |
| 63 | 43 | }), |
| 64 | 44 | ); |
| 65 | 45 | }); |
apps/server/src/terminal/Layers/NodePTY.ts+44-39| ... | ... | @@ -1,55 +1,48 @@ |
| 1 | import fs from "node:fs"; | |
| 2 | 1 | import { createRequire } from "node:module"; |
| 3 | import path from "node:path"; | |
| 4 | 2 | |
| 5 | import { Effect, Layer } from "effect"; | |
| 6 | import { PtyAdapter, PtyExitEvent, PtyProcess, PtySpawnInput } from "../Services/PTY"; | |
| 3 | import { Effect, FileSystem, Layer, Path } from "effect"; | |
| 4 | import { PtyAdapter, PtyAdapterShape, PtyExitEvent, PtyProcess } from "../Services/PTY"; | |
| 7 | 5 | |
| 8 | const requireForNodePty = createRequire(import.meta.url); | |
| 9 | 6 | let didEnsureSpawnHelperExecutable = false; |
| 10 | 7 | |
| 11 | function resolveNodePtySpawnHelperPath(): string | null { | |
| 12 | try { | |
| 13 | const packageJsonPath = requireForNodePty.resolve("node-pty/package.json"); | |
| 14 | const packageDir = path.dirname(packageJsonPath); | |
| 15 | const candidates = [ | |
| 16 | path.join(packageDir, "build", "Release", "spawn-helper"), | |
| 17 | path.join(packageDir, "build", "Debug", "spawn-helper"), | |
| 18 | path.join(packageDir, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"), | |
| 19 | ]; | |
| 20 | ||
| 21 | for (const candidate of candidates) { | |
| 22 | if (fs.existsSync(candidate)) { | |
| 23 | return candidate; | |
| 24 | } | |
| 8 | const resolveNodePtySpawnHelperPath = Effect.gen(function* () { | |
| 9 | const requireForNodePty = createRequire(import.meta.url); | |
| 10 | const path = yield* Path.Path; | |
| 11 | const fs = yield* FileSystem.FileSystem; | |
| 12 | ||
| 13 | const packageJsonPath = requireForNodePty.resolve("node-pty/package.json"); | |
| 14 | const packageDir = path.dirname(packageJsonPath); | |
| 15 | const candidates = [ | |
| 16 | path.join(packageDir, "build", "Release", "spawn-helper"), | |
| 17 | path.join(packageDir, "build", "Debug", "spawn-helper"), | |
| 18 | path.join(packageDir, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"), | |
| 19 | ]; | |
| 20 | ||
| 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)); | |
| 32 | 28 | |
| 33 | export function ensureNodePtySpawnHelperExecutable(explicitPath?: string): void { | |
| 29 | export const ensureNodePtySpawnHelperExecutable = Effect.fn(function* (explicitPath?: string) { | |
| 30 | const fs = yield* FileSystem.FileSystem; | |
| 34 | 31 | if (process.platform === "win32") return; |
| 35 | 32 | if (!explicitPath && didEnsureSpawnHelperExecutable) return; |
| 36 | 33 | |
| 37 | const helperPath = explicitPath ?? resolveNodePtySpawnHelperPath(); | |
| 34 | const helperPath = explicitPath ?? (yield* resolveNodePtySpawnHelperPath); | |
| 38 | 35 | if (!helperPath) return; |
| 39 | 36 | if (!explicitPath) { |
| 40 | 37 | didEnsureSpawnHelperExecutable = true; |
| 41 | 38 | } |
| 42 | 39 | |
| 43 | try { | |
| 44 | const stat = fs.statSync(helperPath); | |
| 45 | const mode = stat.mode & 0o777; | |
| 46 | if ((mode & 0o111) === 0) { | |
| 47 | fs.chmodSync(helperPath, mode | 0o111); | |
| 48 | } | |
| 49 | } catch { | |
| 50 | // Best effort only. If chmod fails, node-pty spawn will surface the real error. | |
| 40 | const stat = yield* fs.stat(helperPath); | |
| 41 | const mode = stat.mode & 0o777; | |
| 42 | if ((mode & 0o111) === 0) { | |
| 43 | yield* fs.chmod(helperPath, mode | 0o111); | |
| 51 | 44 | } |
| 52 | } | |
| 45 | }); | |
| 53 | 46 | |
| 54 | 47 | class NodePtyProcess implements PtyProcess { |
| 55 | 48 | constructor(private readonly process: import("node-pty").IPty) {} |
| ... | ... | @@ -93,10 +86,22 @@ class NodePtyProcess implements PtyProcess { |
| 93 | 86 | export const NodePtyAdapterLive = Layer.effect( |
| 94 | 87 | PtyAdapter, |
| 95 | 88 | Effect.gen(function* () { |
| 89 | const fs = yield* FileSystem.FileSystem; | |
| 90 | const path = yield* Path.Path; | |
| 91 | ||
| 96 | 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 | 102 | return { |
| 98 | spawn(input: PtySpawnInput): PtyProcess { | |
| 99 | ensureNodePtySpawnHelperExecutable(); | |
| 103 | spawn: Effect.fn(function* (input) { | |
| 104 | yield* ensureNodePtySpawnHelperExecutableCached; | |
| 100 | 105 | const ptyProcess = nodePty.spawn(input.shell, input.args ?? [], { |
| 101 | 106 | cwd: input.cwd, |
| 102 | 107 | cols: input.cols, |
| ... | ... | @@ -105,7 +110,7 @@ export const NodePtyAdapterLive = Layer.effect( |
| 105 | 110 | name: globalThis.process.platform === "win32" ? "xterm-color" : "xterm-256color", |
| 106 | 111 | }); |
| 107 | 112 | return new NodePtyProcess(ptyProcess); |
| 108 | }, | |
| 109 | }; | |
| 113 | }), | |
| 114 | } satisfies PtyAdapterShape; | |
| 110 | 115 | }), |
| 111 | 116 | ); |
apps/server/src/terminal/Services/Manager.ts+17-8| ... | ... | @@ -17,7 +17,12 @@ import { |
| 17 | 17 | TerminalWriteInput, |
| 18 | 18 | } from "@t3tools/contracts"; |
| 19 | 19 | import { PtyProcess } from "./PTY"; |
| 20 | import { Effect, ServiceMap } from "effect"; | |
| 20 | import { Effect, Schema, ServiceMap } from "effect"; | |
| 21 | ||
| 22 | export class TerminalError extends Schema.TaggedErrorClass<TerminalError>()("TerminalError", { | |
| 23 | message: Schema.String, | |
| 24 | cause: Schema.optional(Schema.Defect), | |
| 25 | }) {} | |
| 21 | 26 | |
| 22 | 27 | export interface TerminalSessionState { |
| 23 | 28 | threadId: string; |
| ... | ... | @@ -58,36 +63,40 @@ export interface TerminalManagerShape { |
| 58 | 63 | * Reuses an existing session for the same thread/terminal id and restores |
| 59 | 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>; | |
| 62 | 69 | |
| 63 | 70 | /** |
| 64 | 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>; | |
| 67 | 74 | |
| 68 | 75 | /** |
| 69 | 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>; | |
| 72 | 79 | |
| 73 | 80 | /** |
| 74 | 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>; | |
| 77 | 84 | |
| 78 | 85 | /** |
| 79 | 86 | * Restart a terminal session in place. |
| 80 | 87 | * |
| 81 | 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>; | |
| 84 | 93 | |
| 85 | 94 | /** |
| 86 | 95 | * Close an active terminal session. |
| 87 | 96 | * |
| 88 | 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>; | |
| 91 | 100 | |
| 92 | 101 | /** |
| 93 | 102 | * Subscribe to terminal runtime events. |
| ... | ... | @@ -97,7 +106,7 @@ export interface TerminalManagerShape { |
| 97 | 106 | /** |
| 98 | 107 | * Dispose all managed terminal resources. |
| 99 | 108 | */ |
| 100 | readonly dispose: () => Effect.Effect<void>; | |
| 109 | readonly dispose: Effect.Effect<void>; | |
| 101 | 110 | } |
| 102 | 111 | |
| 103 | 112 | /** |
apps/server/src/terminal/Services/PTY.ts+2-2| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | * |
| 7 | 7 | * @module PtyAdapter |
| 8 | 8 | */ |
| 9 | import { ServiceMap } from "effect"; | |
| 9 | import { Effect, ServiceMap } from "effect"; | |
| 10 | 10 | |
| 11 | 11 | export interface PtyExitEvent { |
| 12 | 12 | exitCode: number; |
| ... | ... | @@ -38,7 +38,7 @@ export interface PtyAdapterShape { |
| 38 | 38 | /** |
| 39 | 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 | } |
| 43 | 43 | |
| 44 | 44 | /** |