| author | |
| committer | |
| log | 58053d146b1ea7201884867d1543ccbbc4c05ffd |
| tree | 4d3cd7a36a856fa6aa6673d7a453493989e49b9f |
| parent | 50767683b396cf7065bb18072d6d14c05475b6e9 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 39 | readonly resizeCalls: Array<{ cols: number; rows: number }> = []; |
| 40 | 40 | readonly killSignals: Array<string | undefined> = []; |
| 41 | 41 | readonly pid: number; |
| 42 | writeFailure: unknown | undefined; | |
| 43 | resizeFailure: unknown | undefined; | |
| 42 | 44 | private readonly dataListeners = new Set<(data: string) => void>(); |
| 43 | 45 | private readonly exitListeners = new Set<(event: PtyAdapter.PtyExitEvent) => void>(); |
| 44 | 46 | killed = false; |
| ... | ... | @@ -48,10 +50,16 @@ class FakePtyProcess implements PtyAdapter.PtyProcess { |
| 48 | 50 | } |
| 49 | 51 | |
| 50 | 52 | write(data: string): void { |
| 53 | if (this.writeFailure !== undefined) { | |
| 54 | throw this.writeFailure; | |
| 55 | } | |
| 51 | 56 | this.writes.push(data); |
| 52 | 57 | } |
| 53 | 58 | |
| 54 | 59 | resize(cols: number, rows: number): void { |
| 60 | if (this.resizeFailure !== undefined) { | |
| 61 | throw this.resizeFailure; | |
| 62 | } | |
| 55 | 63 | this.resizeCalls.push({ cols, rows }); |
| 56 | 64 | } |
| 57 | 65 | |
| ... | ... | @@ -435,6 +443,39 @@ it.layer( |
| 435 | 443 | fs.writeFileString(filePath, contents), |
| 436 | 444 | ); |
| 437 | 445 | |
| 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 | 479 | it.effect("preserves non-notFound cwd stat failures", () => |
| 439 | 480 | Effect.gen(function* () { |
| 440 | 481 | if ((yield* HostProcessPlatform) === "win32") return; |
| ... | ... | @@ -452,9 +493,11 @@ it.layer( |
| 452 | 493 | ); |
| 453 | 494 | |
| 454 | 495 | expect(error).toMatchObject({ |
| 455 | _tag: "TerminalCwdError", | |
| 496 | _tag: "TerminalCwdStatError", | |
| 456 | 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 | 541 | }), |
| 499 | 542 | ); |
| 500 | 543 | |
| 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 | 598 | it.effect("ignores delayed resize requests after a terminal closes", () => |
| 502 | 599 | Effect.gen(function* () { |
| 503 | 600 | const { manager, ptyAdapter } = yield* createManager(); |
apps/server/src/terminal/Manager.ts+87-35| ... | ... | @@ -9,10 +9,15 @@ |
| 9 | 9 | import { |
| 10 | 10 | DEFAULT_TERMINAL_ID, |
| 11 | 11 | TerminalCwdError, |
| 12 | TerminalCwdNotDirectoryError, | |
| 13 | TerminalCwdNotFoundError, | |
| 14 | TerminalCwdStatError, | |
| 12 | 15 | TerminalError, |
| 13 | 16 | TerminalHistoryError, |
| 14 | 17 | TerminalNotRunningError, |
| 18 | TerminalResizeError, | |
| 15 | 19 | TerminalSessionLookupError, |
| 20 | TerminalWriteError, | |
| 16 | 21 | type TerminalAttachInput, |
| 17 | 22 | type TerminalAttachStreamEvent, |
| 18 | 23 | type TerminalClearInput, |
| ... | ... | @@ -58,10 +63,15 @@ import * as PtyAdapter from "./PtyAdapter.ts"; |
| 58 | 63 | |
| 59 | 64 | export { |
| 60 | 65 | TerminalCwdError, |
| 66 | TerminalCwdNotDirectoryError, | |
| 67 | TerminalCwdNotFoundError, | |
| 68 | TerminalCwdStatError, | |
| 61 | 69 | TerminalError, |
| 62 | 70 | TerminalHistoryError, |
| 63 | 71 | TerminalNotRunningError, |
| 72 | TerminalResizeError, | |
| 64 | 73 | TerminalSessionLookupError, |
| 74 | TerminalWriteError, | |
| 65 | 75 | }; |
| 66 | 76 | |
| 67 | 77 | const DEFAULT_HISTORY_LINE_LIMIT = 5_000; |
| ... | ... | @@ -190,6 +200,25 @@ interface TerminalSubprocessInspector { |
| 190 | 200 | ): Effect.Effect<TerminalSubprocessInspectResult, TerminalSubprocessCheckError>; |
| 191 | 201 | } |
| 192 | 202 | |
| 203 | const 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 | ||
| 193 | 222 | export interface ShellCandidate { |
| 194 | 223 | shell: string; |
| 195 | 224 | args?: string[]; |
| ... | ... | @@ -1157,16 +1186,6 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func |
| 1157 | 1186 | const legacyHistoryPath = (threadId: string) => |
| 1158 | 1187 | path.join(logsDir, `${legacySafeThreadId(threadId)}.log`); |
| 1159 | 1188 | |
| 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 | 1189 | const readManagerState = SynchronizedRef.get(managerStateRef); |
| 1171 | 1190 | |
| 1172 | 1191 | const modifyManagerState = <A>( |
| ... | ... | @@ -1250,7 +1269,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func |
| 1250 | 1269 | threadId, |
| 1251 | 1270 | terminalId, |
| 1252 | 1271 | signal: "SIGTERM", |
| 1253 | error: error.message, | |
| 1272 | cause: error, | |
| 1254 | 1273 | }).pipe(Effect.as(false)), |
| 1255 | 1274 | ), |
| 1256 | 1275 | ); |
| ... | ... | @@ -1274,7 +1293,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func |
| 1274 | 1293 | threadId, |
| 1275 | 1294 | terminalId, |
| 1276 | 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 | 1391 | if ( |
| 1373 | 1392 | yield* fileSystem |
| 1374 | 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 | 1400 | const raw = yield* fileSystem |
| 1378 | 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 | 1407 | const capped = capHistory(raw, historyLineLimit); |
| 1381 | 1408 | if (capped !== raw) { |
| 1382 | 1409 | yield* fileSystem |
| 1383 | 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 | 1418 | return capped; |
| 1387 | 1419 | } |
| ... | ... | @@ -1394,18 +1426,33 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func |
| 1394 | 1426 | if ( |
| 1395 | 1427 | !(yield* fileSystem |
| 1396 | 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 | 1436 | return ""; |
| 1400 | 1437 | } |
| 1401 | 1438 | |
| 1402 | 1439 | const raw = yield* fileSystem |
| 1403 | 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 | 1447 | const capped = capHistory(raw, historyLineLimit); |
| 1406 | 1448 | yield* fileSystem |
| 1407 | 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 | 1456 | yield* fileSystem.remove(legacyPath, { force: true }).pipe( |
| 1410 | 1457 | Effect.catch((cleanupError) => |
| 1411 | 1458 | Effect.logWarning("failed to remove legacy terminal history", { |
| ... | ... | @@ -1472,20 +1519,15 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func |
| 1472 | 1519 | |
| 1473 | 1520 | const assertValidCwd = Effect.fn("terminal.assertValidCwd")(function* (cwd: string) { |
| 1474 | 1521 | const stats = yield* fileSystem.stat(cwd).pipe( |
| 1475 | Effect.mapError( | |
| 1476 | (cause) => | |
| 1477 | new TerminalCwdError({ | |
| 1478 | cwd, | |
| 1479 | reason: cause.reason._tag === "NotFound" ? "notFound" : "statFailed", | |
| 1480 | cause, | |
| 1481 | }), | |
| 1482 | ), | |
| 1522 | Effect.catchTags({ | |
| 1523 | PlatformError: (cause) => | |
| 1524 | cause.reason._tag === "NotFound" | |
| 1525 | ? new TerminalCwdNotFoundError({ cwd }) | |
| 1526 | : new TerminalCwdStatError({ cwd, cause }), | |
| 1527 | }), | |
| 1483 | 1528 | ); |
| 1484 | 1529 | if (stats.type !== "Directory") { |
| 1485 | return yield* new TerminalCwdError({ | |
| 1486 | cwd, | |
| 1487 | reason: "notDirectory", | |
| 1488 | }); | |
| 1530 | return yield* new TerminalCwdNotDirectoryError({ cwd }); | |
| 1489 | 1531 | } |
| 1490 | 1532 | }); |
| 1491 | 1533 | |
| ... | ... | @@ -1881,7 +1923,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func |
| 1881 | 1923 | yield* Effect.logError("failed to start terminal", { |
| 1882 | 1924 | threadId: session.threadId, |
| 1883 | 1925 | terminalId: session.terminalId, |
| 1884 | error: message, | |
| 1926 | cause: error, | |
| 1885 | 1927 | ...(startedShell ? { shell: startedShell } : {}), |
| 1886 | 1928 | }); |
| 1887 | 1929 | } |
| ... | ... | @@ -2168,10 +2210,10 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func |
| 2168 | 2210 | } |
| 2169 | 2211 | |
| 2170 | 2212 | if (liveSession.cols !== targetCols || liveSession.rows !== targetRows) { |
| 2213 | yield* resizePtyProcess(liveSession, liveSession.process, targetCols, targetRows); | |
| 2171 | 2214 | liveSession.cols = targetCols; |
| 2172 | 2215 | liveSession.rows = targetRows; |
| 2173 | 2216 | liveSession.updatedAt = yield* nowIso; |
| 2174 | liveSession.process.resize(targetCols, targetRows); | |
| 2175 | 2217 | } |
| 2176 | 2218 | |
| 2177 | 2219 | return snapshot(liveSession); |
| ... | ... | @@ -2219,10 +2261,11 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func |
| 2219 | 2261 | session.status === "running" && |
| 2220 | 2262 | (session.cols !== targetCols || session.rows !== targetRows) |
| 2221 | 2263 | ) { |
| 2264 | const process = session.process; | |
| 2265 | yield* resizePtyProcess(session, process, targetCols, targetRows); | |
| 2222 | 2266 | session.cols = targetCols; |
| 2223 | 2267 | session.rows = targetRows; |
| 2224 | 2268 | session.updatedAt = yield* nowIso; |
| 2225 | yield* Effect.sync(() => session.process?.resize(targetCols, targetRows)); | |
| 2226 | 2269 | } |
| 2227 | 2270 | |
| 2228 | 2271 | return snapshot(session); |
| ... | ... | @@ -2409,7 +2452,16 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func |
| 2409 | 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 | }); |
| 2414 | 2466 | |
| 2415 | 2467 | const resizeLocked = Effect.fn("terminal.resize")(function* (input: TerminalResizeInput) { |
| ... | ... | @@ -2422,10 +2474,10 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func |
| 2422 | 2474 | if (!process || session.value.status !== "running") { |
| 2423 | 2475 | return; |
| 2424 | 2476 | } |
| 2477 | yield* resizePtyProcess(session.value, process, input.cols, input.rows); | |
| 2425 | 2478 | session.value.cols = input.cols; |
| 2426 | 2479 | session.value.rows = input.rows; |
| 2427 | 2480 | session.value.updatedAt = yield* nowIso; |
| 2428 | yield* Effect.sync(() => process.resize(input.cols, input.rows)); | |
| 2429 | 2481 | }); |
| 2430 | 2482 | |
| 2431 | 2483 | const resize: TerminalManager["Service"]["resize"] = (input) => |
packages/contracts/src/terminal.ts+65-20| ... | ... | @@ -232,34 +232,47 @@ export const TerminalAttachStreamEvent = Schema.Union([ |
| 232 | 232 | ]); |
| 233 | 233 | export type TerminalAttachStreamEvent = typeof TerminalAttachStreamEvent.Type; |
| 234 | 234 | |
| 235 | export class TerminalCwdError extends Schema.TaggedErrorClass<TerminalCwdError>()( | |
| 236 | "TerminalCwdError", | |
| 235 | export class TerminalCwdNotFoundError extends Schema.TaggedErrorClass<TerminalCwdNotFoundError>()( | |
| 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 | ||
| 246 | export class TerminalCwdNotDirectoryError extends Schema.TaggedErrorClass<TerminalCwdNotDirectoryError>()( | |
| 247 | "TerminalCwdNotDirectoryError", | |
| 237 | 248 | { |
| 238 | 249 | cwd: Schema.String, |
| 239 | reason: Schema.Literals(["notFound", "notDirectory", "statFailed"]), | |
| 240 | cause: Schema.optional(Schema.Defect()), | |
| 241 | 250 | }, |
| 242 | 251 | ) { |
| 243 | 252 | override get message() { |
| 244 | if (this.reason === "notDirectory") { | |
| 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}`; | |
| 253 | return `Terminal cwd is not a directory: ${this.cwd}`; | |
| 260 | 254 | } |
| 261 | 255 | } |
| 262 | 256 | |
| 257 | export 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 | ||
| 269 | export const TerminalCwdError = Schema.Union([ | |
| 270 | TerminalCwdNotFoundError, | |
| 271 | TerminalCwdNotDirectoryError, | |
| 272 | TerminalCwdStatError, | |
| 273 | ]); | |
| 274 | export type TerminalCwdError = typeof TerminalCwdError.Type; | |
| 275 | ||
| 263 | 276 | export class TerminalHistoryError extends Schema.TaggedErrorClass<TerminalHistoryError>()( |
| 264 | 277 | "TerminalHistoryError", |
| 265 | 278 | { |
| ... | ... | @@ -298,10 +311,42 @@ export class TerminalNotRunningError extends Schema.TaggedErrorClass<TerminalNot |
| 298 | 311 | } |
| 299 | 312 | } |
| 300 | 313 | |
| 314 | export 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 | ||
| 328 | export 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 | ||
| 301 | 344 | export const TerminalError = Schema.Union([ |
| 302 | 345 | TerminalCwdError, |
| 303 | 346 | TerminalHistoryError, |
| 304 | 347 | TerminalSessionLookupError, |
| 305 | 348 | TerminalNotRunningError, |
| 349 | TerminalWriteError, | |
| 350 | TerminalResizeError, | |
| 306 | 351 | ]); |
| 307 | 352 | export type TerminalError = typeof TerminalError.Type; |