Skip to content
Source
Sign inSign up

terminal.ts

384 lines12.6 KB

1import * as Schema from "effect/Schema";
2import { TrimmedNonEmptyString } from "./baseSchemas.ts";
3import { ProviderInstanceId } from "./providerInstance.ts";
4​
5export const DEFAULT_TERMINAL_ID = "term-1";
6export const DEFAULT_MAX_TERMINAL_BUFFER_BYTES = 512 * 1024;
7​
8const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;
9const TerminalColsSchema = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).check(
10 Schema.isLessThanOrEqualTo(1000),
11);
12const TerminalRowsSchema = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).check(
13 Schema.isLessThanOrEqualTo(500),
14);
15const TerminalIdSchema = TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(128));
16const TerminalEnvKeySchema = Schema.String.check(
17 Schema.isPattern(/^[A-Za-z_][A-Za-z0-9_]*$/),
18).check(Schema.isMaxLength(128));
19const TerminalEnvValueSchema = Schema.String.check(Schema.isMaxLength(8_192));
20const TerminalEnvSchema = Schema.Record(TerminalEnvKeySchema, TerminalEnvValueSchema).check(
21 Schema.isMaxProperties(128),
22);
23​
24export const TerminalThreadInput = Schema.Struct({
25 threadId: TrimmedNonEmptyStringSchema,
26});
27export type TerminalThreadInput = typeof TerminalThreadInput.Type;
28​
29// Terminal ids are client-chosen; the server never allocates them.
30const TerminalSessionInput = Schema.Struct({
31 ...TerminalThreadInput.fields,
32 terminalId: TerminalIdSchema,
33});
34export type TerminalSessionInput = Schema.Codec.Encoded<typeof TerminalSessionInput>;
35​
36export const TerminalOpenInput = Schema.Struct({
37 ...TerminalSessionInput.fields,
38 cwd: TrimmedNonEmptyStringSchema,
39 worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyStringSchema)),
40 cols: Schema.optional(TerminalColsSchema),
41 rows: Schema.optional(TerminalRowsSchema),
42 env: Schema.optional(TerminalEnvSchema),
43 providerInstanceId: Schema.optional(ProviderInstanceId),
44 stopOnSettle: Schema.optional(Schema.Boolean),
45});
46export type TerminalOpenInput = typeof TerminalOpenInput.Type;
47​
48export const TerminalAttachInput = Schema.Struct({
49 ...TerminalSessionInput.fields,
50 cwd: Schema.optional(TrimmedNonEmptyStringSchema),
51 worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyStringSchema)),
52 cols: Schema.optional(TerminalColsSchema),
53 rows: Schema.optional(TerminalRowsSchema),
54 env: Schema.optional(TerminalEnvSchema),
55 providerInstanceId: Schema.optional(ProviderInstanceId),
56 stopOnSettle: Schema.optional(Schema.Boolean),
57 restartIfNotRunning: Schema.optional(Schema.Boolean),
58 // Resubscriptions preserve stopped terminals; unknown IDs recover after a server restart.
59 resume: Schema.optional(Schema.Boolean),
60});
61export type TerminalAttachInput = typeof TerminalAttachInput.Type;
62​
63export const TerminalObserveInput = TerminalSessionInput;
64export type TerminalObserveInput = Schema.Codec.Encoded<typeof TerminalObserveInput>;
65​
66export const TerminalWriteInput = Schema.Struct({
67 ...TerminalSessionInput.fields,
68 data: Schema.String.check(Schema.isNonEmpty()).check(Schema.isMaxLength(65_536)),
69});
70export type TerminalWriteInput = Schema.Codec.Encoded<typeof TerminalWriteInput>;
71​
72export const TerminalResizeInput = Schema.Struct({
73 ...TerminalSessionInput.fields,
74 cols: TerminalColsSchema,
75 rows: TerminalRowsSchema,
76});
77export type TerminalResizeInput = Schema.Codec.Encoded<typeof TerminalResizeInput>;
78​
79export const TerminalClearInput = TerminalSessionInput;
80export type TerminalClearInput = Schema.Codec.Encoded<typeof TerminalClearInput>;
81​
82export const TerminalCloseInput = Schema.Struct({
83 ...TerminalThreadInput.fields,
84 terminalId: Schema.optional(TerminalIdSchema),
85 deleteHistory: Schema.optional(Schema.Boolean),
86});
87export type TerminalCloseInput = typeof TerminalCloseInput.Type;
88​
89export const TerminalSessionStatus = Schema.Literals(["starting", "running", "exited", "error"]);
90export type TerminalSessionStatus = typeof TerminalSessionStatus.Type;
91​
92export const TerminalSessionSnapshot = Schema.Struct({
93 threadId: Schema.String.check(Schema.isNonEmpty()),
94 terminalId: Schema.String.check(Schema.isNonEmpty()),
95 cwd: Schema.String.check(Schema.isNonEmpty()),
96 worktreePath: Schema.NullOr(TrimmedNonEmptyStringSchema),
97 status: TerminalSessionStatus,
98 pid: Schema.NullOr(Schema.Int.check(Schema.isGreaterThan(0))),
99 history: Schema.String,
100 exitCode: Schema.NullOr(Schema.Int),
101 exitSignal: Schema.NullOr(Schema.Int),
102 label: Schema.String.check(Schema.isMaxLength(128)),
103 updatedAt: Schema.String,
104 sequence: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
105 // The PTY grid every renderer of this terminal should draw; absent from older servers.
106 cols: Schema.optional(TerminalColsSchema),
107 rows: Schema.optional(TerminalRowsSchema),
108});
109export type TerminalSessionSnapshot = typeof TerminalSessionSnapshot.Type;
110​
111export const TerminalSummary = Schema.Struct({
112 threadId: Schema.String.check(Schema.isNonEmpty()),
113 terminalId: Schema.String.check(Schema.isNonEmpty()),
114 cwd: Schema.String.check(Schema.isNonEmpty()),
115 worktreePath: Schema.NullOr(TrimmedNonEmptyStringSchema),
116 status: TerminalSessionStatus,
117 pid: Schema.NullOr(Schema.Int.check(Schema.isGreaterThan(0))),
118 exitCode: Schema.NullOr(Schema.Int),
119 exitSignal: Schema.NullOr(Schema.Int),
120 hasRunningSubprocess: Schema.Boolean,
121 /**
122 * Exit code of the last command the live shell finished, from OSC 133/633
123 * shell-integration marks. Absent when the shell reports none or the last
124 * mark had no code.
125 */
126 lastCommandExitCode: Schema.optionalKey(Schema.Int),
127 label: Schema.String.check(Schema.isMaxLength(128)),
128 updatedAt: Schema.String,
129});
130export type TerminalSummary = typeof TerminalSummary.Type;
131​
132const TerminalMetadataSnapshotEvent = Schema.Struct({
133 type: Schema.Literal("snapshot"),
134 terminals: Schema.Array(TerminalSummary),
135});
136​
137const TerminalMetadataUpsertEvent = Schema.Struct({
138 type: Schema.Literal("upsert"),
139 terminal: TerminalSummary,
140});
141​
142const TerminalMetadataRemoveEvent = Schema.Struct({
143 type: Schema.Literal("remove"),
144 threadId: Schema.String.check(Schema.isNonEmpty()),
145 terminalId: Schema.String.check(Schema.isNonEmpty()),
146});
147​
148export const TerminalMetadataStreamEvent = Schema.Union([
149 TerminalMetadataSnapshotEvent,
150 TerminalMetadataUpsertEvent,
151 TerminalMetadataRemoveEvent,
152]);
153export type TerminalMetadataStreamEvent = typeof TerminalMetadataStreamEvent.Type;
154​
155const TerminalEventBaseSchema = Schema.Struct({
156 threadId: Schema.String.check(Schema.isNonEmpty()),
157 terminalId: Schema.String.check(Schema.isNonEmpty()),
158 sequence: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
159});
160​
161const TerminalStartedEvent = Schema.Struct({
162 ...TerminalEventBaseSchema.fields,
163 type: Schema.Literal("started"),
164 snapshot: TerminalSessionSnapshot,
165});
166​
167const TerminalOutputEvent = Schema.Struct({
168 ...TerminalEventBaseSchema.fields,
169 type: Schema.Literal("output"),
170 data: Schema.String,
171});
172​
173const TerminalExitedEvent = Schema.Struct({
174 ...TerminalEventBaseSchema.fields,
175 type: Schema.Literal("exited"),
176 exitCode: Schema.NullOr(Schema.Int),
177 exitSignal: Schema.NullOr(Schema.Int),
178});
179​
180const TerminalClosedEvent = Schema.Struct({
181 ...TerminalEventBaseSchema.fields,
182 type: Schema.Literal("closed"),
183});
184​
185const TerminalErrorEvent = Schema.Struct({
186 ...TerminalEventBaseSchema.fields,
187 type: Schema.Literal("error"),
188 message: Schema.String.check(Schema.isNonEmpty()),
189});
190​
191const TerminalClearedEvent = Schema.Struct({
192 ...TerminalEventBaseSchema.fields,
193 type: Schema.Literal("cleared"),
194});
195​
196const TerminalRestartedEvent = Schema.Struct({
197 ...TerminalEventBaseSchema.fields,
198 type: Schema.Literal("restarted"),
199 snapshot: TerminalSessionSnapshot,
200});
201​
202const TerminalActivityEvent = Schema.Struct({
203 ...TerminalEventBaseSchema.fields,
204 type: Schema.Literal("activity"),
205 hasRunningSubprocess: Schema.Boolean,
206 lastCommandExitCode: Schema.optionalKey(Schema.Int),
207 label: Schema.String.check(Schema.isMaxLength(128)),
208 // Shared PTY grid changes ride on activity because older clients reject unknown event types.
209 cols: Schema.optional(TerminalColsSchema),
210 rows: Schema.optional(TerminalRowsSchema),
211});
212​
213export const TerminalEvent = Schema.Union([
214 TerminalStartedEvent,
215 TerminalOutputEvent,
216 TerminalExitedEvent,
217 TerminalClosedEvent,
218 TerminalErrorEvent,
219 TerminalClearedEvent,
220 TerminalRestartedEvent,
221 TerminalActivityEvent,
222]);
223export type TerminalEvent = typeof TerminalEvent.Type;
224​
225const TerminalAttachSnapshotEvent = Schema.Struct({
226 type: Schema.Literal("snapshot"),
227 snapshot: TerminalSessionSnapshot,
228});
229​
230export const TerminalAttachStreamEvent = Schema.Union([
231 TerminalAttachSnapshotEvent,
232 TerminalOutputEvent,
233 TerminalExitedEvent,
234 TerminalClosedEvent,
235 TerminalErrorEvent,
236 TerminalClearedEvent,
237 TerminalRestartedEvent,
238 TerminalActivityEvent,
239]);
240export type TerminalAttachStreamEvent = typeof TerminalAttachStreamEvent.Type;
241​
242export class TerminalCwdNotFoundError extends Schema.TaggedError<TerminalCwdNotFoundError>()(
243 "TerminalCwdNotFoundError",
244 {
245 cwd: Schema.String,
246 },
247) {
248 override get message() {
249 return `Terminal cwd does not exist: ${this.cwd}`;
250 }
252​
253export class TerminalCwdNotDirectoryError extends Schema.TaggedError<TerminalCwdNotDirectoryError>()(
254 "TerminalCwdNotDirectoryError",
255 {
256 cwd: Schema.String,
257 },
258) {
259 override get message() {
260 return `Terminal cwd is not a directory: ${this.cwd}`;
261 }
263​
264export class TerminalCwdStatError extends Schema.TaggedError<TerminalCwdStatError>()(
265 "TerminalCwdStatError",
266 {
267 cwd: Schema.String,
268 cause: Schema.Defect(),
269 },
270) {
271 override get message() {
272 return `Failed to access terminal cwd: ${this.cwd}`;
273 }
275​
276export const TerminalCwdError = Schema.Union([
277 TerminalCwdNotFoundError,
278 TerminalCwdNotDirectoryError,
279 TerminalCwdStatError,
280]);
281export type TerminalCwdError = typeof TerminalCwdError.Type;
282​
283export class TerminalHistoryError extends Schema.TaggedError<TerminalHistoryError>()(
284 "TerminalHistoryError",
285 {
286 operation: Schema.Literals(["read", "truncate", "migrate"]),
287 threadId: Schema.String,
288 terminalId: Schema.String,
289 cause: Schema.optional(Schema.Defect()),
290 },
291) {
292 override get message() {
293 return `Failed to ${this.operation} terminal history for thread: ${this.threadId}, terminal: ${this.terminalId}`;
294 }
296​
297export class TerminalSessionLookupError extends Schema.TaggedError<TerminalSessionLookupError>()(
298 "TerminalSessionLookupError",
299 {
300 threadId: Schema.String,
301 terminalId: Schema.String,
302 },
303) {
304 override get message() {
305 return `Unknown terminal thread: ${this.threadId}, terminal: ${this.terminalId}`;
306 }
308​
309export class TerminalProviderInstanceNotFoundError extends Schema.TaggedError<TerminalProviderInstanceNotFoundError>()(
310 "TerminalProviderInstanceNotFoundError",
311 {
312 providerInstanceId: ProviderInstanceId,
313 },
314) {
315 override get message() {
316 return `Provider instance is not available: ${this.providerInstanceId}`;
317 }
319​
320export class TerminalProviderEnvironmentError extends Schema.TaggedError<TerminalProviderEnvironmentError>()(
321 "TerminalProviderEnvironmentError",
322 {
323 providerInstanceId: ProviderInstanceId,
324 cause: Schema.Defect(),
325 },
326) {
327 override get message() {
328 return `Could not prepare the terminal environment for provider instance: ${this.providerInstanceId}`;
329 }
331​
332export class TerminalNotRunningError extends Schema.TaggedError<TerminalNotRunningError>()(
333 "TerminalNotRunningError",
334 {
335 threadId: Schema.String,
336 terminalId: Schema.String,
337 },
338) {
339 override get message() {
340 return `Terminal is not running for thread: ${this.threadId}, terminal: ${this.terminalId}`;
341 }
343​
344export class TerminalWriteError extends Schema.TaggedError<TerminalWriteError>()(
345 "TerminalWriteError",
346 {
347 threadId: Schema.String,
348 terminalId: Schema.String,
349 terminalPid: Schema.Number,
350 cause: Schema.Defect(),
351 },
352) {
353 override get message() {
354 return `Failed to write to terminal for thread: ${this.threadId}, terminal: ${this.terminalId}, PID: ${this.terminalPid}`;
355 }
357​
358export class TerminalResizeError extends Schema.TaggedError<TerminalResizeError>()(
359 "TerminalResizeError",
360 {
361 threadId: Schema.String,
362 terminalId: Schema.String,
363 terminalPid: Schema.Number,
364 cols: TerminalColsSchema,
365 rows: TerminalRowsSchema,
366 cause: Schema.Defect(),
367 },
368) {
369 override get message() {
370 return `Failed to resize terminal for thread: ${this.threadId}, terminal: ${this.terminalId}, PID: ${this.terminalPid} to ${this.cols}x${this.rows}`;
371 }
373​
374export const TerminalError = Schema.Union([
375 TerminalCwdError,
376 TerminalHistoryError,
377 TerminalSessionLookupError,
378 TerminalProviderInstanceNotFoundError,
379 TerminalProviderEnvironmentError,
380 TerminalNotRunningError,
381 TerminalWriteError,
382 TerminalResizeError,
383]);
384export type TerminalError = typeof TerminalError.Type;