authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 15:12:54-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 15:12:54-07:00
log58053d146b1ea7201884867d1543ccbbc4c05ffd
tree4d3cd7a36a856fa6aa6673d7a453493989e49b9f
parent50767683b396cf7065bb18072d6d14c05475b6e9
signature Signed by PGP key B5690EEEBB952194

[codex] Structure terminal PTY operation failures (#3364)

Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 251 insertions(+), 57 deletions(-)

apps/server/src/terminal/Manager.test.ts+99-2
...@@ -39,6 +39,8 @@ class FakePtyProcess implements PtyAdapter.PtyProcess {...@@ -39,6 +39,8 @@ class FakePtyProcess implements PtyAdapter.PtyProcess {
39 readonly resizeCalls: Array<{ cols: number; rows: number }> = [];39 readonly resizeCalls: Array<{ cols: number; rows: number }> = [];
40 readonly killSignals: Array<string | undefined> = [];40 readonly killSignals: Array<string | undefined> = [];
41 readonly pid: number;41 readonly pid: number;
42 writeFailure: unknown | undefined;
43 resizeFailure: unknown | undefined;
42 private readonly dataListeners = new Set<(data: string) => void>();44 private readonly dataListeners = new Set<(data: string) => void>();
43 private readonly exitListeners = new Set<(event: PtyAdapter.PtyExitEvent) => void>();45 private readonly exitListeners = new Set<(event: PtyAdapter.PtyExitEvent) => void>();
44 killed = false;46 killed = false;
...@@ -48,10 +50,16 @@ class FakePtyProcess implements PtyAdapter.PtyProcess {...@@ -48,10 +50,16 @@ class FakePtyProcess implements PtyAdapter.PtyProcess {
48 }50 }
4951
50 write(data: string): void {52 write(data: string): void {
53 if (this.writeFailure !== undefined) {
54 throw this.writeFailure;
55 }
51 this.writes.push(data);56 this.writes.push(data);
52 }57 }
5358
54 resize(cols: number, rows: number): void {59 resize(cols: number, rows: number): void {
60 if (this.resizeFailure !== undefined) {
61 throw this.resizeFailure;
62 }
55 this.resizeCalls.push({ cols, rows });63 this.resizeCalls.push({ cols, rows });
56 }64 }
5765
...@@ -435,6 +443,39 @@ it.layer(...@@ -435,6 +443,39 @@ it.layer(
435 fs.writeFileString(filePath, contents),443 fs.writeFileString(filePath, contents),
436 );444 );
437445
446 it.effect("reports a missing cwd without an artificial cause", () =>
447 Effect.gen(function* () {
448 const path = yield* Path.Path;
449
450 const { manager, baseDir } = yield* createManager();
451 const cwd = path.join(baseDir, "missing-cwd");
452 const error = yield* Effect.flip(manager.open(openInput({ cwd })));
453
454 expect(error).toMatchObject({
455 _tag: "TerminalCwdNotFoundError",
456 cwd,
457 });
458 expect("cause" in error).toBe(false);
459 }),
460 );
461
462 it.effect("reports a cwd that is not a directory", () =>
463 Effect.gen(function* () {
464 const path = yield* Path.Path;
465
466 const { manager, baseDir } = yield* createManager();
467 const cwd = path.join(baseDir, "cwd-file");
468 yield* writeFileString(cwd, "not a directory");
469 const error = yield* Effect.flip(manager.open(openInput({ cwd })));
470
471 expect(error).toMatchObject({
472 _tag: "TerminalCwdNotDirectoryError",
473 cwd,
474 });
475 expect("cause" in error).toBe(false);
476 }),
477 );
478
438 it.effect("preserves non-notFound cwd stat failures", () =>479 it.effect("preserves non-notFound cwd stat failures", () =>
439 Effect.gen(function* () {480 Effect.gen(function* () {
440 if ((yield* HostProcessPlatform) === "win32") return;481 if ((yield* HostProcessPlatform) === "win32") return;
...@@ -452,9 +493,11 @@ it.layer(...@@ -452,9 +493,11 @@ it.layer(
452 );493 );
453494
454 expect(error).toMatchObject({495 expect(error).toMatchObject({
455 _tag: "TerminalCwdError",496 _tag: "TerminalCwdStatError",
456 cwd: blockedCwd,497 cwd: blockedCwd,
457 reason: "statFailed",498 cause: {
499 _tag: "PlatformError",
500 },
458 });501 });
459 }),502 }),
460 );503 );
...@@ -498,6 +541,60 @@ it.layer(...@@ -498,6 +541,60 @@ it.layer(
498 }),541 }),
499 );542 );
500543
544 it.effect("preserves structured context and causes for PTY I/O failures", () =>
545 Effect.gen(function* () {
546 const { manager, ptyAdapter } = yield* createManager();
547 yield* manager.open(openInput());
548 const process = ptyAdapter.processes[0];
549 expect(process).toBeDefined();
550 if (!process) return;
551
552 const writeCause = new Error("PTY input handle is unavailable");
553 process.writeFailure = writeCause;
554 const writeError = yield* Effect.flip(
555 manager.write({
556 threadId: "thread-1",
557 terminalId: DEFAULT_TERMINAL_ID,
558 data: "secret input that must not be attached to the error",
559 }),
560 );
561
562 expect(writeError).toMatchObject({
563 _tag: "TerminalWriteError",
564 threadId: "thread-1",
565 terminalId: DEFAULT_TERMINAL_ID,
566 terminalPid: process.pid,
567 });
568 expect(writeError.cause).toBe(writeCause);
569 expect(writeError).not.toHaveProperty("data");
570
571 const resizeCause = new Error("PTY resize handle is unavailable");
572 process.resizeFailure = resizeCause;
573 const resizeError = yield* Effect.flip(
574 manager.resize({
575 threadId: "thread-1",
576 terminalId: DEFAULT_TERMINAL_ID,
577 cols: 132,
578 rows: 40,
579 }),
580 );
581
582 expect(resizeError).toMatchObject({
583 _tag: "TerminalResizeError",
584 threadId: "thread-1",
585 terminalId: DEFAULT_TERMINAL_ID,
586 terminalPid: process.pid,
587 cols: 132,
588 rows: 40,
589 });
590 expect(resizeError.cause).toBe(resizeCause);
591
592 process.resizeFailure = undefined;
593 yield* manager.open(openInput({ cols: 132, rows: 40 }));
594 expect(process.resizeCalls).toEqual([{ cols: 132, rows: 40 }]);
595 }),
596 );
597
501 it.effect("ignores delayed resize requests after a terminal closes", () =>598 it.effect("ignores delayed resize requests after a terminal closes", () =>
502 Effect.gen(function* () {599 Effect.gen(function* () {
503 const { manager, ptyAdapter } = yield* createManager();600 const { manager, ptyAdapter } = yield* createManager();
apps/server/src/terminal/Manager.ts+87-35
...@@ -9,10 +9,15 @@...@@ -9,10 +9,15 @@
9import {9import {
10 DEFAULT_TERMINAL_ID,10 DEFAULT_TERMINAL_ID,
11 TerminalCwdError,11 TerminalCwdError,
12 TerminalCwdNotDirectoryError,
13 TerminalCwdNotFoundError,
14 TerminalCwdStatError,
12 TerminalError,15 TerminalError,
13 TerminalHistoryError,16 TerminalHistoryError,
14 TerminalNotRunningError,17 TerminalNotRunningError,
18 TerminalResizeError,
15 TerminalSessionLookupError,19 TerminalSessionLookupError,
20 TerminalWriteError,
16 type TerminalAttachInput,21 type TerminalAttachInput,
17 type TerminalAttachStreamEvent,22 type TerminalAttachStreamEvent,
18 type TerminalClearInput,23 type TerminalClearInput,
...@@ -58,10 +63,15 @@ import * as PtyAdapter from "./PtyAdapter.ts";...@@ -58,10 +63,15 @@ import * as PtyAdapter from "./PtyAdapter.ts";
5863
59export {64export {
60 TerminalCwdError,65 TerminalCwdError,
66 TerminalCwdNotDirectoryError,
67 TerminalCwdNotFoundError,
68 TerminalCwdStatError,
61 TerminalError,69 TerminalError,
62 TerminalHistoryError,70 TerminalHistoryError,
63 TerminalNotRunningError,71 TerminalNotRunningError,
72 TerminalResizeError,
64 TerminalSessionLookupError,73 TerminalSessionLookupError,
74 TerminalWriteError,
65};75};
6676
67const DEFAULT_HISTORY_LINE_LIMIT = 5_000;77const DEFAULT_HISTORY_LINE_LIMIT = 5_000;
...@@ -190,6 +200,25 @@ interface TerminalSubprocessInspector {...@@ -190,6 +200,25 @@ interface TerminalSubprocessInspector {
190 ): Effect.Effect<TerminalSubprocessInspectResult, TerminalSubprocessCheckError>;200 ): Effect.Effect<TerminalSubprocessInspectResult, TerminalSubprocessCheckError>;
191}201}
192202
203const resizePtyProcess = (
204 session: TerminalSessionState,
205 process: PtyAdapter.PtyProcess,
206 cols: number,
207 rows: number,
208) =>
209 Effect.try({
210 try: () => process.resize(cols, rows),
211 catch: (cause) =>
212 new TerminalResizeError({
213 threadId: session.threadId,
214 terminalId: session.terminalId,
215 terminalPid: process.pid,
216 cols,
217 rows,
218 cause,
219 }),
220 });
221
193export interface ShellCandidate {222export interface ShellCandidate {
194 shell: string;223 shell: string;
195 args?: string[];224 args?: string[];
...@@ -1157,16 +1186,6 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -1157,16 +1186,6 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
1157 const legacyHistoryPath = (threadId: string) =>1186 const legacyHistoryPath = (threadId: string) =>
1158 path.join(logsDir, `${legacySafeThreadId(threadId)}.log`);1187 path.join(logsDir, `${legacySafeThreadId(threadId)}.log`);
11591188
1160 const toTerminalHistoryError =
1161 (operation: "read" | "truncate" | "migrate", threadId: string, terminalId: string) =>
1162 (cause: unknown) =>
1163 new TerminalHistoryError({
1164 operation,
1165 threadId,
1166 terminalId,
1167 cause,
1168 });
1169
1170 const readManagerState = SynchronizedRef.get(managerStateRef);1189 const readManagerState = SynchronizedRef.get(managerStateRef);
11711190
1172 const modifyManagerState = <A>(1191 const modifyManagerState = <A>(
...@@ -1250,7 +1269,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -1250,7 +1269,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
1250 threadId,1269 threadId,
1251 terminalId,1270 terminalId,
1252 signal: "SIGTERM",1271 signal: "SIGTERM",
1253 error: error.message,1272 cause: error,
1254 }).pipe(Effect.as(false)),1273 }).pipe(Effect.as(false)),
1255 ),1274 ),
1256 );1275 );
...@@ -1274,7 +1293,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -1274,7 +1293,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
1274 threadId,1293 threadId,
1275 terminalId,1294 terminalId,
1276 signal: "SIGKILL",1295 signal: "SIGKILL",
1277 error: error.message,1296 cause: error,
1278 }),1297 }),
1279 ),1298 ),
1280 );1299 );
...@@ -1372,16 +1391,29 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -1372,16 +1391,29 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
1372 if (1391 if (
1373 yield* fileSystem1392 yield* fileSystem
1374 .exists(nextPath)1393 .exists(nextPath)
1375 .pipe(Effect.mapError(toTerminalHistoryError("read", threadId, terminalId)))1394 .pipe(
1395 Effect.mapError(
1396 (cause) => new TerminalHistoryError({ operation: "read", threadId, terminalId, cause }),
1397 ),
1398 )
1376 ) {1399 ) {
1377 const raw = yield* fileSystem1400 const raw = yield* fileSystem
1378 .readFileString(nextPath)1401 .readFileString(nextPath)
1379 .pipe(Effect.mapError(toTerminalHistoryError("read", threadId, terminalId)));1402 .pipe(
1403 Effect.mapError(
1404 (cause) => new TerminalHistoryError({ operation: "read", threadId, terminalId, cause }),
1405 ),
1406 );
1380 const capped = capHistory(raw, historyLineLimit);1407 const capped = capHistory(raw, historyLineLimit);
1381 if (capped !== raw) {1408 if (capped !== raw) {
1382 yield* fileSystem1409 yield* fileSystem
1383 .writeFileString(nextPath, capped)1410 .writeFileString(nextPath, capped)
1384 .pipe(Effect.mapError(toTerminalHistoryError("truncate", threadId, terminalId)));1411 .pipe(
1412 Effect.mapError(
1413 (cause) =>
1414 new TerminalHistoryError({ operation: "truncate", threadId, terminalId, cause }),
1415 ),
1416 );
1385 }1417 }
1386 return capped;1418 return capped;
1387 }1419 }
...@@ -1394,18 +1426,33 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -1394,18 +1426,33 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
1394 if (1426 if (
1395 !(yield* fileSystem1427 !(yield* fileSystem
1396 .exists(legacyPath)1428 .exists(legacyPath)
1397 .pipe(Effect.mapError(toTerminalHistoryError("migrate", threadId, terminalId))))1429 .pipe(
1430 Effect.mapError(
1431 (cause) =>
1432 new TerminalHistoryError({ operation: "migrate", threadId, terminalId, cause }),
1433 ),
1434 ))
1398 ) {1435 ) {
1399 return "";1436 return "";
1400 }1437 }
14011438
1402 const raw = yield* fileSystem1439 const raw = yield* fileSystem
1403 .readFileString(legacyPath)1440 .readFileString(legacyPath)
1404 .pipe(Effect.mapError(toTerminalHistoryError("migrate", threadId, terminalId)));1441 .pipe(
1442 Effect.mapError(
1443 (cause) =>
1444 new TerminalHistoryError({ operation: "migrate", threadId, terminalId, cause }),
1445 ),
1446 );
1405 const capped = capHistory(raw, historyLineLimit);1447 const capped = capHistory(raw, historyLineLimit);
1406 yield* fileSystem1448 yield* fileSystem
1407 .writeFileString(nextPath, capped)1449 .writeFileString(nextPath, capped)
1408 .pipe(Effect.mapError(toTerminalHistoryError("migrate", threadId, terminalId)));1450 .pipe(
1451 Effect.mapError(
1452 (cause) =>
1453 new TerminalHistoryError({ operation: "migrate", threadId, terminalId, cause }),
1454 ),
1455 );
1409 yield* fileSystem.remove(legacyPath, { force: true }).pipe(1456 yield* fileSystem.remove(legacyPath, { force: true }).pipe(
1410 Effect.catch((cleanupError) =>1457 Effect.catch((cleanupError) =>
1411 Effect.logWarning("failed to remove legacy terminal history", {1458 Effect.logWarning("failed to remove legacy terminal history", {
...@@ -1472,20 +1519,15 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -1472,20 +1519,15 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
14721519
1473 const assertValidCwd = Effect.fn("terminal.assertValidCwd")(function* (cwd: string) {1520 const assertValidCwd = Effect.fn("terminal.assertValidCwd")(function* (cwd: string) {
1474 const stats = yield* fileSystem.stat(cwd).pipe(1521 const stats = yield* fileSystem.stat(cwd).pipe(
1475 Effect.mapError(1522 Effect.catchTags({
1476 (cause) =>1523 PlatformError: (cause) =>
1477 new TerminalCwdError({1524 cause.reason._tag === "NotFound"
1478 cwd,1525 ? new TerminalCwdNotFoundError({ cwd })
1479 reason: cause.reason._tag === "NotFound" ? "notFound" : "statFailed",1526 : new TerminalCwdStatError({ cwd, cause }),
1480 cause,1527 }),
1481 }),
1482 ),
1483 );1528 );
1484 if (stats.type !== "Directory") {1529 if (stats.type !== "Directory") {
1485 return yield* new TerminalCwdError({1530 return yield* new TerminalCwdNotDirectoryError({ cwd });
1486 cwd,
1487 reason: "notDirectory",
1488 });
1489 }1531 }
1490 });1532 });
14911533
...@@ -1881,7 +1923,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -1881,7 +1923,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
1881 yield* Effect.logError("failed to start terminal", {1923 yield* Effect.logError("failed to start terminal", {
1882 threadId: session.threadId,1924 threadId: session.threadId,
1883 terminalId: session.terminalId,1925 terminalId: session.terminalId,
1884 error: message,1926 cause: error,
1885 ...(startedShell ? { shell: startedShell } : {}),1927 ...(startedShell ? { shell: startedShell } : {}),
1886 });1928 });
1887 }1929 }
...@@ -2168,10 +2210,10 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -2168,10 +2210,10 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
2168 }2210 }
21692211
2170 if (liveSession.cols !== targetCols || liveSession.rows !== targetRows) {2212 if (liveSession.cols !== targetCols || liveSession.rows !== targetRows) {
2213 yield* resizePtyProcess(liveSession, liveSession.process, targetCols, targetRows);
2171 liveSession.cols = targetCols;2214 liveSession.cols = targetCols;
2172 liveSession.rows = targetRows;2215 liveSession.rows = targetRows;
2173 liveSession.updatedAt = yield* nowIso;2216 liveSession.updatedAt = yield* nowIso;
2174 liveSession.process.resize(targetCols, targetRows);
2175 }2217 }
21762218
2177 return snapshot(liveSession);2219 return snapshot(liveSession);
...@@ -2219,10 +2261,11 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -2219,10 +2261,11 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
2219 session.status === "running" &&2261 session.status === "running" &&
2220 (session.cols !== targetCols || session.rows !== targetRows)2262 (session.cols !== targetCols || session.rows !== targetRows)
2221 ) {2263 ) {
2264 const process = session.process;
2265 yield* resizePtyProcess(session, process, targetCols, targetRows);
2222 session.cols = targetCols;2266 session.cols = targetCols;
2223 session.rows = targetRows;2267 session.rows = targetRows;
2224 session.updatedAt = yield* nowIso;2268 session.updatedAt = yield* nowIso;
2225 yield* Effect.sync(() => session.process?.resize(targetCols, targetRows));
2226 }2269 }
22272270
2228 return snapshot(session);2271 return snapshot(session);
...@@ -2409,7 +2452,16 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -2409,7 +2452,16 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
2409 terminalId,2452 terminalId,
2410 });2453 });
2411 }2454 }
2412 yield* Effect.sync(() => process.write(input.data));2455 yield* Effect.try({
2456 try: () => process.write(input.data),
2457 catch: (cause) =>
2458 new TerminalWriteError({
2459 threadId: input.threadId,
2460 terminalId,
2461 terminalPid: process.pid,
2462 cause,
2463 }),
2464 });
2413 });2465 });
24142466
2415 const resizeLocked = Effect.fn("terminal.resize")(function* (input: TerminalResizeInput) {2467 const resizeLocked = Effect.fn("terminal.resize")(function* (input: TerminalResizeInput) {
...@@ -2422,10 +2474,10 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -2422,10 +2474,10 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
2422 if (!process || session.value.status !== "running") {2474 if (!process || session.value.status !== "running") {
2423 return;2475 return;
2424 }2476 }
2477 yield* resizePtyProcess(session.value, process, input.cols, input.rows);
2425 session.value.cols = input.cols;2478 session.value.cols = input.cols;
2426 session.value.rows = input.rows;2479 session.value.rows = input.rows;
2427 session.value.updatedAt = yield* nowIso;2480 session.value.updatedAt = yield* nowIso;
2428 yield* Effect.sync(() => process.resize(input.cols, input.rows));
2429 });2481 });
24302482
2431 const resize: TerminalManager["Service"]["resize"] = (input) =>2483 const resize: TerminalManager["Service"]["resize"] = (input) =>
packages/contracts/src/terminal.ts+65-20
...@@ -232,34 +232,47 @@ export const TerminalAttachStreamEvent = Schema.Union([...@@ -232,34 +232,47 @@ export const TerminalAttachStreamEvent = Schema.Union([
232]);232]);
233export type TerminalAttachStreamEvent = typeof TerminalAttachStreamEvent.Type;233export type TerminalAttachStreamEvent = typeof TerminalAttachStreamEvent.Type;
234234
235export class TerminalCwdError extends Schema.TaggedErrorClass<TerminalCwdError>()(235export class TerminalCwdNotFoundError extends Schema.TaggedErrorClass<TerminalCwdNotFoundError>()(
236 "TerminalCwdError",236 "TerminalCwdNotFoundError",
237 {
238 cwd: Schema.String,
239 },
240) {
241 override get message() {
242 return `Terminal cwd does not exist: ${this.cwd}`;
243 }
244}
245
246export class TerminalCwdNotDirectoryError extends Schema.TaggedErrorClass<TerminalCwdNotDirectoryError>()(
247 "TerminalCwdNotDirectoryError",
237 {248 {
238 cwd: Schema.String,249 cwd: Schema.String,
239 reason: Schema.Literals(["notFound", "notDirectory", "statFailed"]),
240 cause: Schema.optional(Schema.Defect()),
241 },250 },
242) {251) {
243 override get message() {252 override get message() {
244 if (this.reason === "notDirectory") {253 return `Terminal cwd is not a directory: ${this.cwd}`;
245 return `Terminal cwd is not a directory: ${this.cwd}`;
246 }
247 if (this.reason === "notFound") {
248 return `Terminal cwd does not exist: ${this.cwd}`;
249 }
250 const causeMessage =
251 this.cause !== undefined &&
252 this.cause !== null &&
253 typeof this.cause === "object" &&
254 "message" in this.cause
255 ? this.cause.message
256 : undefined;
257 return typeof causeMessage === "string" && causeMessage.length > 0
258 ? `Failed to access terminal cwd: ${this.cwd} (${causeMessage})`
259 : `Failed to access terminal cwd: ${this.cwd}`;
260 }254 }
261}255}
262256
257export class TerminalCwdStatError extends Schema.TaggedErrorClass<TerminalCwdStatError>()(
258 "TerminalCwdStatError",
259 {
260 cwd: Schema.String,
261 cause: Schema.Defect(),
262 },
263) {
264 override get message() {
265 return `Failed to access terminal cwd: ${this.cwd}`;
266 }
267}
268
269export const TerminalCwdError = Schema.Union([
270 TerminalCwdNotFoundError,
271 TerminalCwdNotDirectoryError,
272 TerminalCwdStatError,
273]);
274export type TerminalCwdError = typeof TerminalCwdError.Type;
275
263export class TerminalHistoryError extends Schema.TaggedErrorClass<TerminalHistoryError>()(276export class TerminalHistoryError extends Schema.TaggedErrorClass<TerminalHistoryError>()(
264 "TerminalHistoryError",277 "TerminalHistoryError",
265 {278 {
...@@ -298,10 +311,42 @@ export class TerminalNotRunningError extends Schema.TaggedErrorClass<TerminalNot...@@ -298,10 +311,42 @@ export class TerminalNotRunningError extends Schema.TaggedErrorClass<TerminalNot
298 }311 }
299}312}
300313
314export class TerminalWriteError extends Schema.TaggedErrorClass<TerminalWriteError>()(
315 "TerminalWriteError",
316 {
317 threadId: Schema.String,
318 terminalId: Schema.String,
319 terminalPid: Schema.Number,
320 cause: Schema.Defect(),
321 },
322) {
323 override get message() {
324 return `Failed to write to terminal for thread: ${this.threadId}, terminal: ${this.terminalId}, PID: ${this.terminalPid}`;
325 }
326}
327
328export class TerminalResizeError extends Schema.TaggedErrorClass<TerminalResizeError>()(
329 "TerminalResizeError",
330 {
331 threadId: Schema.String,
332 terminalId: Schema.String,
333 terminalPid: Schema.Number,
334 cols: TerminalColsSchema,
335 rows: TerminalRowsSchema,
336 cause: Schema.Defect(),
337 },
338) {
339 override get message() {
340 return `Failed to resize terminal for thread: ${this.threadId}, terminal: ${this.terminalId}, PID: ${this.terminalPid} to ${this.cols}x${this.rows}`;
341 }
342}
343
301export const TerminalError = Schema.Union([344export const TerminalError = Schema.Union([
302 TerminalCwdError,345 TerminalCwdError,
303 TerminalHistoryError,346 TerminalHistoryError,
304 TerminalSessionLookupError,347 TerminalSessionLookupError,
305 TerminalNotRunningError,348 TerminalNotRunningError,
349 TerminalWriteError,
350 TerminalResizeError,
306]);351]);
307export type TerminalError = typeof TerminalError.Type;352export type TerminalError = typeof TerminalError.Type;