1import * as Schema from "effect/Schema";
2import { TrimmedNonEmptyString } from "./baseSchemas.ts";
3import { ProviderInstanceId } from "./providerInstance.ts";
4
5/**
6 * Client-side id for the first shell opened on a thread. Ids are uniformly
7 * `term-N`; there's no "default" intrinsic. Kept as a named constant so callers
8 * that want "the primary shell" don't hardcode `"term-1"`.
9 */
10export const DEFAULT_TERMINAL_ID = "term-1";
11
12const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;
13const TerminalColsSchema = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).check(
14 Schema.isLessThanOrEqualTo(1000),
15);
16const TerminalRowsSchema = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).check(
17 Schema.isLessThanOrEqualTo(500),
18);
19const TerminalIdSchema = TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(128));
20const TerminalEnvKeySchema = Schema.String.check(
21 Schema.isPattern(/^[A-Za-z_][A-Za-z0-9_]*$/),
22).check(Schema.isMaxLength(128));
23const TerminalEnvValueSchema = Schema.String.check(Schema.isMaxLength(8_192));
24const TerminalEnvSchema = Schema.Record(TerminalEnvKeySchema, TerminalEnvValueSchema).check(
25 Schema.isMaxProperties(128),
26);
27
28export const TerminalThreadInput = Schema.Struct({
29 threadId: TrimmedNonEmptyStringSchema,
30});
31export type TerminalThreadInput = typeof TerminalThreadInput.Type;
32
33/** Terminal ids are ALWAYS chosen by the client and sent explicitly — no server-side allocation. */
34const TerminalSessionInput = Schema.Struct({
35 ...TerminalThreadInput.fields,
36 terminalId: TerminalIdSchema,
37});
38export type TerminalSessionInput = Schema.Codec.Encoded<typeof TerminalSessionInput>;
39
40export const TerminalOpenInput = Schema.Struct({
41 ...TerminalSessionInput.fields,
42 cwd: TrimmedNonEmptyStringSchema,
43 worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyStringSchema)),
44 cols: Schema.optional(TerminalColsSchema),
45 rows: Schema.optional(TerminalRowsSchema),
46 env: Schema.optional(TerminalEnvSchema),
47 providerInstanceId: Schema.optional(ProviderInstanceId),
48});
49export type TerminalOpenInput = typeof TerminalOpenInput.Type;
50
51export const TerminalAttachInput = Schema.Struct({
52 ...TerminalSessionInput.fields,
53 cwd: Schema.optional(TrimmedNonEmptyStringSchema),
54 worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyStringSchema)),
55 cols: Schema.optional(TerminalColsSchema),
56 rows: Schema.optional(TerminalRowsSchema),
57 env: Schema.optional(TerminalEnvSchema),
58 providerInstanceId: Schema.optional(ProviderInstanceId),
59 restartIfNotRunning: 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 TerminalRestartInput = Schema.Struct({
83 ...TerminalSessionInput.fields,
84 cwd: TrimmedNonEmptyStringSchema,
85 worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyStringSchema)),
86 cols: TerminalColsSchema,
87 rows: TerminalRowsSchema,
88 env: Schema.optional(TerminalEnvSchema),
89 providerInstanceId: Schema.optional(ProviderInstanceId),
90});
91export type TerminalRestartInput = typeof TerminalRestartInput.Type;
92
93export const TerminalCloseInput = Schema.Struct({
94 ...TerminalThreadInput.fields,
95 terminalId: Schema.optional(TerminalIdSchema),
96 deleteHistory: Schema.optional(Schema.Boolean),
97});
98export type TerminalCloseInput = typeof TerminalCloseInput.Type;
99
100export const TerminalSessionStatus = Schema.Literals(["starting", "running", "exited", "error"]);
101export type TerminalSessionStatus = typeof TerminalSessionStatus.Type;
102
103export const TerminalSessionSnapshot = Schema.Struct({
104 threadId: Schema.String.check(Schema.isNonEmpty()),
105 terminalId: Schema.String.check(Schema.isNonEmpty()),
106 cwd: Schema.String.check(Schema.isNonEmpty()),
107 worktreePath: Schema.NullOr(TrimmedNonEmptyStringSchema),
108 status: TerminalSessionStatus,
109 pid: Schema.NullOr(Schema.Int.check(Schema.isGreaterThan(0))),
110 history: Schema.String,
111 exitCode: Schema.NullOr(Schema.Int),
112 exitSignal: Schema.NullOr(Schema.Int),
113 /** Server-computed display title (idle shell vs subprocess command). */
114 label: Schema.String.check(Schema.isMaxLength(128)),
115 updatedAt: Schema.String,
116 sequence: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
117});
118export type TerminalSessionSnapshot = typeof TerminalSessionSnapshot.Type;
119
120export const TerminalSummary = Schema.Struct({
121 threadId: Schema.String.check(Schema.isNonEmpty()),
122 terminalId: Schema.String.check(Schema.isNonEmpty()),
123 cwd: Schema.String.check(Schema.isNonEmpty()),
124 worktreePath: Schema.NullOr(TrimmedNonEmptyStringSchema),
125 status: TerminalSessionStatus,
126 pid: Schema.NullOr(Schema.Int.check(Schema.isGreaterThan(0))),
127 exitCode: Schema.NullOr(Schema.Int),
128 exitSignal: Schema.NullOr(Schema.Int),
129 hasRunningSubprocess: Schema.Boolean,
130 /** Server-computed display title (idle shell vs subprocess command). */
131 label: Schema.String.check(Schema.isMaxLength(128)),
132 updatedAt: Schema.String,
133});
134export type TerminalSummary = typeof TerminalSummary.Type;
135
136const TerminalMetadataSnapshotEvent = Schema.Struct({
137 type: Schema.Literal("snapshot"),
138 terminals: Schema.Array(TerminalSummary),
139});
140
141const TerminalMetadataUpsertEvent = Schema.Struct({
142 type: Schema.Literal("upsert"),
143 terminal: TerminalSummary,
144});
145
146const TerminalMetadataRemoveEvent = Schema.Struct({
147 type: Schema.Literal("remove"),
148 threadId: Schema.String.check(Schema.isNonEmpty()),
149 terminalId: Schema.String.check(Schema.isNonEmpty()),
150});
151
152export const TerminalMetadataStreamEvent = Schema.Union([
153 TerminalMetadataSnapshotEvent,
154 TerminalMetadataUpsertEvent,
155 TerminalMetadataRemoveEvent,
156]);
157export type TerminalMetadataStreamEvent = typeof TerminalMetadataStreamEvent.Type;
158
159const TerminalEventBaseSchema = Schema.Struct({
160 threadId: Schema.String.check(Schema.isNonEmpty()),
161 terminalId: Schema.String.check(Schema.isNonEmpty()),
162 sequence: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
163});
164
165const TerminalStartedEvent = Schema.Struct({
166 ...TerminalEventBaseSchema.fields,
167 type: Schema.Literal("started"),
168 snapshot: TerminalSessionSnapshot,
169});
170
171const TerminalOutputEvent = Schema.Struct({
172 ...TerminalEventBaseSchema.fields,
173 type: Schema.Literal("output"),
174 data: Schema.String,
175});
176
177const TerminalExitedEvent = Schema.Struct({
178 ...TerminalEventBaseSchema.fields,
179 type: Schema.Literal("exited"),
180 exitCode: Schema.NullOr(Schema.Int),
181 exitSignal: Schema.NullOr(Schema.Int),
182});
183
184const TerminalClosedEvent = Schema.Struct({
185 ...TerminalEventBaseSchema.fields,
186 type: Schema.Literal("closed"),
187});
188
189const TerminalErrorEvent = Schema.Struct({
190 ...TerminalEventBaseSchema.fields,
191 type: Schema.Literal("error"),
192 message: Schema.String.check(Schema.isNonEmpty()),
193});
194
195const TerminalClearedEvent = Schema.Struct({
196 ...TerminalEventBaseSchema.fields,
197 type: Schema.Literal("cleared"),
198});
199
200const TerminalRestartedEvent = Schema.Struct({
201 ...TerminalEventBaseSchema.fields,
202 type: Schema.Literal("restarted"),
203 snapshot: TerminalSessionSnapshot,
204});
205
206const TerminalActivityEvent = Schema.Struct({
207 ...TerminalEventBaseSchema.fields,
208 type: Schema.Literal("activity"),
209 hasRunningSubprocess: Schema.Boolean,
210 label: Schema.String.check(Schema.isMaxLength(128)),
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 }
251}
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 }
262}
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 }
274}
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 }
295}
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 }
307}
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 }
318}
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 }
330}
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 }
342}
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 }
356}
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 }
372}
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;