@jmoyers/harness 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/native/ptyd/Cargo.lock +16 -0
  4. package/native/ptyd/Cargo.toml +7 -0
  5. package/native/ptyd/src/main.rs +257 -0
  6. package/package.json +90 -0
  7. package/scripts/build-ptyd.sh +73 -0
  8. package/scripts/control-plane-daemon.ts +277 -0
  9. package/scripts/cursor-hook-relay.ts +82 -0
  10. package/scripts/harness-animate.ts +469 -0
  11. package/scripts/harness-bin.js +77 -0
  12. package/scripts/harness-core.ts +1 -0
  13. package/scripts/harness-inspector.ts +439 -0
  14. package/scripts/harness.ts +2493 -0
  15. package/src/adapters/agent-session-state.ts +390 -0
  16. package/src/cli/gateway-record.ts +173 -0
  17. package/src/codex/live-session.ts +872 -0
  18. package/src/config/config-core.ts +1359 -0
  19. package/src/config/secrets-core.ts +170 -0
  20. package/src/control-plane/agent-realtime-api.ts +2441 -0
  21. package/src/control-plane/codex-session-stream.ts +392 -0
  22. package/src/control-plane/codex-telemetry.ts +1325 -0
  23. package/src/control-plane/lifecycle-hooks.ts +706 -0
  24. package/src/control-plane/session-summary.ts +380 -0
  25. package/src/control-plane/status/agent-status-reducer.ts +21 -0
  26. package/src/control-plane/status/reducer-base.ts +170 -0
  27. package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
  28. package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
  29. package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
  30. package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
  31. package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
  32. package/src/control-plane/status/session-status-engine.ts +76 -0
  33. package/src/control-plane/stream-client.ts +396 -0
  34. package/src/control-plane/stream-command-parser.ts +1673 -0
  35. package/src/control-plane/stream-protocol.ts +1808 -0
  36. package/src/control-plane/stream-server-background.ts +486 -0
  37. package/src/control-plane/stream-server-command.ts +2557 -0
  38. package/src/control-plane/stream-server-connection.ts +234 -0
  39. package/src/control-plane/stream-server-observed-filter.ts +112 -0
  40. package/src/control-plane/stream-server-session-runtime.ts +566 -0
  41. package/src/control-plane/stream-server-state-store.ts +15 -0
  42. package/src/control-plane/stream-server.ts +3192 -0
  43. package/src/cursor/managed-hooks.ts +282 -0
  44. package/src/domain/conversations.ts +414 -0
  45. package/src/domain/directories.ts +78 -0
  46. package/src/domain/repositories.ts +123 -0
  47. package/src/domain/tasks.ts +148 -0
  48. package/src/domain/workspace.ts +156 -0
  49. package/src/events/normalized-events.ts +124 -0
  50. package/src/mux/ansi-integrity.ts +103 -0
  51. package/src/mux/control-plane-op-queue.ts +212 -0
  52. package/src/mux/conversation-rail.ts +339 -0
  53. package/src/mux/double-click.ts +78 -0
  54. package/src/mux/dual-pane-core.ts +435 -0
  55. package/src/mux/harness-core-ui.ts +817 -0
  56. package/src/mux/input-shortcuts.ts +667 -0
  57. package/src/mux/live-mux/actions-conversation.ts +344 -0
  58. package/src/mux/live-mux/actions-repository.ts +246 -0
  59. package/src/mux/live-mux/actions-task.ts +115 -0
  60. package/src/mux/live-mux/args.ts +142 -0
  61. package/src/mux/live-mux/command-menu.ts +298 -0
  62. package/src/mux/live-mux/control-plane-records.ts +546 -0
  63. package/src/mux/live-mux/conversation-state.ts +188 -0
  64. package/src/mux/live-mux/directory-resolution.ts +34 -0
  65. package/src/mux/live-mux/event-mapping.ts +96 -0
  66. package/src/mux/live-mux/gateway-profiler.ts +152 -0
  67. package/src/mux/live-mux/gateway-render-trace.ts +177 -0
  68. package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
  69. package/src/mux/live-mux/git-parsing.ts +131 -0
  70. package/src/mux/live-mux/git-snapshot.ts +263 -0
  71. package/src/mux/live-mux/git-state.ts +136 -0
  72. package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
  73. package/src/mux/live-mux/home-pane-actions.ts +58 -0
  74. package/src/mux/live-mux/home-pane-drop.ts +44 -0
  75. package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
  76. package/src/mux/live-mux/home-pane-pointer.ts +96 -0
  77. package/src/mux/live-mux/input-forwarding.ts +112 -0
  78. package/src/mux/live-mux/layout.ts +30 -0
  79. package/src/mux/live-mux/left-nav-activation.ts +103 -0
  80. package/src/mux/live-mux/left-nav.ts +85 -0
  81. package/src/mux/live-mux/left-rail-actions.ts +118 -0
  82. package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
  83. package/src/mux/live-mux/left-rail-pointer.ts +74 -0
  84. package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
  85. package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
  86. package/src/mux/live-mux/modal-input-reducers.ts +94 -0
  87. package/src/mux/live-mux/modal-overlays.ts +287 -0
  88. package/src/mux/live-mux/modal-pointer.ts +70 -0
  89. package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
  90. package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
  91. package/src/mux/live-mux/observed-stream.ts +87 -0
  92. package/src/mux/live-mux/palette-parsing.ts +128 -0
  93. package/src/mux/live-mux/pointer-routing.ts +108 -0
  94. package/src/mux/live-mux/process-usage.ts +53 -0
  95. package/src/mux/live-mux/project-pane-pointer.ts +44 -0
  96. package/src/mux/live-mux/rail-layout.ts +244 -0
  97. package/src/mux/live-mux/render-trace-analysis.ts +213 -0
  98. package/src/mux/live-mux/render-trace-state.ts +84 -0
  99. package/src/mux/live-mux/repository-folding.ts +207 -0
  100. package/src/mux/live-mux/runtime-shutdown.ts +51 -0
  101. package/src/mux/live-mux/selection.ts +411 -0
  102. package/src/mux/live-mux/startup-utils.ts +187 -0
  103. package/src/mux/live-mux/status-timeline-state.ts +82 -0
  104. package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
  105. package/src/mux/live-mux/terminal-palette.ts +79 -0
  106. package/src/mux/new-thread-prompt.ts +165 -0
  107. package/src/mux/project-tree.ts +295 -0
  108. package/src/mux/render-frame.ts +113 -0
  109. package/src/mux/runtime-wiring.ts +185 -0
  110. package/src/mux/selector-index.ts +160 -0
  111. package/src/mux/startup-sequencer.ts +238 -0
  112. package/src/mux/task-composer.ts +289 -0
  113. package/src/mux/task-focused-pane.ts +417 -0
  114. package/src/mux/task-screen-keybindings.ts +539 -0
  115. package/src/mux/terminal-input-modes.ts +35 -0
  116. package/src/mux/workspace-path.ts +55 -0
  117. package/src/mux/workspace-rail-model.ts +701 -0
  118. package/src/mux/workspace-rail.ts +247 -0
  119. package/src/perf/perf-core.ts +307 -0
  120. package/src/pty/pty_host.ts +217 -0
  121. package/src/pty/session-broker.ts +158 -0
  122. package/src/recording/terminal-recording.ts +383 -0
  123. package/src/services/control-plane.ts +567 -0
  124. package/src/services/conversation-lifecycle.ts +176 -0
  125. package/src/services/conversation-startup-hydration.ts +47 -0
  126. package/src/services/directory-hydration.ts +49 -0
  127. package/src/services/event-persistence.ts +104 -0
  128. package/src/services/mux-ui-state-persistence.ts +82 -0
  129. package/src/services/output-load-sampler.ts +231 -0
  130. package/src/services/process-usage-refresh.ts +88 -0
  131. package/src/services/recording.ts +75 -0
  132. package/src/services/render-trace-recorder.ts +177 -0
  133. package/src/services/runtime-control-actions.ts +123 -0
  134. package/src/services/runtime-control-plane-ops.ts +131 -0
  135. package/src/services/runtime-conversation-actions.ts +113 -0
  136. package/src/services/runtime-conversation-activation.ts +78 -0
  137. package/src/services/runtime-conversation-starter.ts +171 -0
  138. package/src/services/runtime-conversation-title-edit.ts +149 -0
  139. package/src/services/runtime-directory-actions.ts +164 -0
  140. package/src/services/runtime-envelope-handler.ts +198 -0
  141. package/src/services/runtime-git-state.ts +92 -0
  142. package/src/services/runtime-input-pipeline.ts +50 -0
  143. package/src/services/runtime-input-router.ts +202 -0
  144. package/src/services/runtime-layout-resize.ts +236 -0
  145. package/src/services/runtime-left-rail-render.ts +159 -0
  146. package/src/services/runtime-main-pane-input.ts +230 -0
  147. package/src/services/runtime-modal-input.ts +119 -0
  148. package/src/services/runtime-navigation-input.ts +207 -0
  149. package/src/services/runtime-process-wiring.ts +68 -0
  150. package/src/services/runtime-rail-input.ts +287 -0
  151. package/src/services/runtime-render-flush.ts +146 -0
  152. package/src/services/runtime-render-lifecycle.ts +104 -0
  153. package/src/services/runtime-render-orchestrator.ts +108 -0
  154. package/src/services/runtime-render-pipeline.ts +167 -0
  155. package/src/services/runtime-render-state.ts +72 -0
  156. package/src/services/runtime-repository-actions.ts +197 -0
  157. package/src/services/runtime-right-pane-render.ts +132 -0
  158. package/src/services/runtime-shutdown.ts +79 -0
  159. package/src/services/runtime-stream-subscriptions.ts +56 -0
  160. package/src/services/runtime-task-composer-persistence.ts +139 -0
  161. package/src/services/runtime-task-editor-actions.ts +83 -0
  162. package/src/services/runtime-task-pane-actions.ts +198 -0
  163. package/src/services/runtime-task-pane-shortcuts.ts +189 -0
  164. package/src/services/runtime-task-pane.ts +62 -0
  165. package/src/services/runtime-workspace-actions.ts +153 -0
  166. package/src/services/runtime-workspace-observed-events.ts +190 -0
  167. package/src/services/session-projection-instrumentation.ts +190 -0
  168. package/src/services/startup-background-probe.ts +91 -0
  169. package/src/services/startup-background-resume.ts +65 -0
  170. package/src/services/startup-orchestrator.ts +166 -0
  171. package/src/services/startup-output-tracker.ts +54 -0
  172. package/src/services/startup-paint-tracker.ts +115 -0
  173. package/src/services/startup-persisted-conversation-queue.ts +45 -0
  174. package/src/services/startup-settled-gate.ts +67 -0
  175. package/src/services/startup-shutdown.ts +53 -0
  176. package/src/services/startup-span-tracker.ts +77 -0
  177. package/src/services/startup-state-hydration.ts +94 -0
  178. package/src/services/startup-visibility.ts +35 -0
  179. package/src/services/status-timeline-recorder.ts +144 -0
  180. package/src/services/task-pane-selection-actions.ts +153 -0
  181. package/src/services/task-planning-hydration.ts +58 -0
  182. package/src/services/task-planning-observed-events.ts +89 -0
  183. package/src/services/workspace-observed-events.ts +113 -0
  184. package/src/store/control-plane-store-normalize.ts +760 -0
  185. package/src/store/control-plane-store-types.ts +224 -0
  186. package/src/store/control-plane-store.ts +2951 -0
  187. package/src/store/event-store.ts +253 -0
  188. package/src/store/sqlite.ts +81 -0
  189. package/src/terminal/compat-matrix.ts +345 -0
  190. package/src/terminal/differential-checkpoints.ts +132 -0
  191. package/src/terminal/parity-suite.ts +441 -0
  192. package/src/terminal/snapshot-oracle.ts +1840 -0
  193. package/src/ui/conversation-input-forwarder.ts +114 -0
  194. package/src/ui/conversation-selection-input.ts +103 -0
  195. package/src/ui/debug-footer-notice.ts +39 -0
  196. package/src/ui/global-shortcut-input.ts +126 -0
  197. package/src/ui/input-preflight.ts +68 -0
  198. package/src/ui/input-token-router.ts +312 -0
  199. package/src/ui/input.ts +238 -0
  200. package/src/ui/kit.ts +509 -0
  201. package/src/ui/left-nav-input.ts +80 -0
  202. package/src/ui/left-rail-pointer-input.ts +148 -0
  203. package/src/ui/main-pane-pointer-input.ts +150 -0
  204. package/src/ui/modals/manager.ts +192 -0
  205. package/src/ui/mux-theme.ts +529 -0
  206. package/src/ui/panes/conversation.ts +19 -0
  207. package/src/ui/panes/home-gridfire.ts +302 -0
  208. package/src/ui/panes/home.ts +109 -0
  209. package/src/ui/panes/left-rail.ts +12 -0
  210. package/src/ui/panes/project.ts +44 -0
  211. package/src/ui/pointer-routing-input.ts +158 -0
  212. package/src/ui/repository-fold-input.ts +91 -0
  213. package/src/ui/screen.ts +210 -0
  214. package/src/ui/surface.ts +224 -0
@@ -0,0 +1,217 @@
1
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import { EventEmitter } from 'node:events';
3
+ import { existsSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import {
7
+ isPerfCoreEnabled,
8
+ perfNowNs,
9
+ recordPerfDuration,
10
+ recordPerfEvent,
11
+ startPerfSpan,
12
+ } from '../perf/perf-core.ts';
13
+
14
+ const OPCODE_DATA = 0x01;
15
+ const OPCODE_RESIZE = 0x02;
16
+ const OPCODE_CLOSE = 0x03;
17
+
18
+ const DEFAULT_COMMAND = '/bin/sh';
19
+ const DEFAULT_COMMAND_ARGS = ['-i'];
20
+ const DEFAULT_HELPER_PATH_CANDIDATES = [
21
+ join(dirname(fileURLToPath(import.meta.url)), '../../native/ptyd/target/release/ptyd'),
22
+ join(dirname(fileURLToPath(import.meta.url)), '../../bin/ptyd'),
23
+ ] as const;
24
+
25
+ interface StartPtySessionOptions {
26
+ command?: string;
27
+ commandArgs?: string[];
28
+ env?: NodeJS.ProcessEnv;
29
+ cwd?: string;
30
+ helperPath?: string;
31
+ initialCols?: number;
32
+ initialRows?: number;
33
+ }
34
+
35
+ export interface PtyExit {
36
+ code: number | null;
37
+ signal: NodeJS.Signals | null;
38
+ }
39
+
40
+ export function resolvePtyHelperPath(
41
+ helperPath: string | undefined,
42
+ helperPathCandidates: readonly string[] = DEFAULT_HELPER_PATH_CANDIDATES,
43
+ pathExists: (path: string) => boolean = existsSync,
44
+ ): string {
45
+ if (typeof helperPath === 'string' && helperPath.length > 0) {
46
+ return helperPath;
47
+ }
48
+ for (const candidate of helperPathCandidates) {
49
+ if (pathExists(candidate)) {
50
+ return candidate;
51
+ }
52
+ }
53
+ const fallback = helperPathCandidates[0];
54
+ if (fallback === undefined) {
55
+ throw new Error('pty helper path candidates must include at least one path');
56
+ }
57
+ return fallback;
58
+ }
59
+
60
+ class PtySession extends EventEmitter {
61
+ private readonly child: ChildProcessWithoutNullStreams;
62
+ private readonly pendingRoundtripProbes: Array<{
63
+ probeId: number;
64
+ payloadLength: number;
65
+ matchPayloads: Buffer[];
66
+ startedAtNs: bigint;
67
+ }> = [];
68
+ private nextProbeId = 1;
69
+ private outputWindow = Buffer.alloc(0);
70
+ private static readonly MAX_OUTPUT_WINDOW_BYTES = 8192;
71
+
72
+ constructor(child: ChildProcessWithoutNullStreams) {
73
+ super();
74
+ this.child = child;
75
+
76
+ child.stdout.on('data', (chunk: Buffer) => {
77
+ this.emit('data', chunk);
78
+ this.trackRoundtrip(chunk);
79
+ });
80
+
81
+ child.on('error', (error: Error) => {
82
+ this.emit('error', error);
83
+ });
84
+
85
+ child.on('exit', (code: number | null, signal: NodeJS.Signals | null) => {
86
+ this.emit('exit', { code, signal } satisfies PtyExit);
87
+ });
88
+ }
89
+
90
+ write(data: string | Uint8Array): void {
91
+ const payload = typeof data === 'string' ? Buffer.from(data, 'utf8') : Buffer.from(data);
92
+ const frame = Buffer.alloc(1 + 4 + payload.length);
93
+ frame[0] = OPCODE_DATA;
94
+ frame.writeUInt32BE(payload.length, 1);
95
+ payload.copy(frame, 5);
96
+
97
+ if (isPerfCoreEnabled() && payload.length > 0 && payload.length <= 256) {
98
+ this.pendingRoundtripProbes.push({
99
+ probeId: this.nextProbeId,
100
+ payloadLength: payload.length,
101
+ matchPayloads: PtySession.buildMatchPayloads(payload),
102
+ startedAtNs: perfNowNs(),
103
+ });
104
+ this.nextProbeId += 1;
105
+ }
106
+
107
+ const span = startPerfSpan('pty.stdin.write', { bytes: payload.length });
108
+ this.child.stdin.write(frame, () => {
109
+ span.end();
110
+ });
111
+ }
112
+
113
+ resize(cols: number, rows: number): void {
114
+ const frame = Buffer.alloc(1 + 2 + 2);
115
+ frame[0] = OPCODE_RESIZE;
116
+ frame.writeUInt16BE(cols, 1);
117
+ frame.writeUInt16BE(rows, 3);
118
+ this.child.stdin.write(frame);
119
+ }
120
+
121
+ close(): void {
122
+ const frame = Buffer.from([OPCODE_CLOSE]);
123
+ this.child.stdin.write(frame);
124
+ }
125
+
126
+ processId(): number | null {
127
+ return typeof this.child.pid === 'number' ? this.child.pid : null;
128
+ }
129
+
130
+ private trackRoundtrip(chunk: Buffer): void {
131
+ if (!isPerfCoreEnabled()) {
132
+ return;
133
+ }
134
+
135
+ recordPerfEvent('pty.stdout.chunk', { bytes: chunk.length });
136
+
137
+ if (this.pendingRoundtripProbes.length === 0) {
138
+ return;
139
+ }
140
+
141
+ this.outputWindow =
142
+ this.outputWindow.length === 0
143
+ ? Buffer.from(chunk)
144
+ : Buffer.concat([this.outputWindow, chunk]);
145
+ if (this.outputWindow.length > PtySession.MAX_OUTPUT_WINDOW_BYTES) {
146
+ this.outputWindow = this.outputWindow.subarray(
147
+ this.outputWindow.length - PtySession.MAX_OUTPUT_WINDOW_BYTES,
148
+ );
149
+ }
150
+
151
+ let idx = 0;
152
+ while (idx < this.pendingRoundtripProbes.length) {
153
+ const probe = this.pendingRoundtripProbes[idx];
154
+ if (
155
+ probe !== undefined &&
156
+ probe.matchPayloads.some((matchPayload) => this.outputWindow.includes(matchPayload))
157
+ ) {
158
+ recordPerfDuration('pty.keystroke.roundtrip', probe.startedAtNs, {
159
+ 'probe-id': probe.probeId,
160
+ bytes: probe.payloadLength,
161
+ });
162
+ this.pendingRoundtripProbes.splice(idx, 1);
163
+ continue;
164
+ }
165
+ idx += 1;
166
+ }
167
+ }
168
+
169
+ private static buildMatchPayloads(payload: Buffer): Buffer[] {
170
+ if (!payload.includes(0x0a)) {
171
+ return [payload];
172
+ }
173
+
174
+ const crlfPayload = Buffer.alloc(
175
+ payload.length + payload.filter((byte) => byte === 0x0a).length,
176
+ );
177
+ let writeIdx = 0;
178
+ for (const byte of payload.values()) {
179
+ if (byte === 0x0a) {
180
+ crlfPayload[writeIdx] = 0x0d;
181
+ writeIdx += 1;
182
+ }
183
+ crlfPayload[writeIdx] = byte;
184
+ writeIdx += 1;
185
+ }
186
+
187
+ return [payload, crlfPayload];
188
+ }
189
+ }
190
+
191
+ export function startPtySession(options: StartPtySessionOptions = {}): PtySession {
192
+ const command = options.command ?? DEFAULT_COMMAND;
193
+ const commandArgs = options.commandArgs ?? DEFAULT_COMMAND_ARGS;
194
+ const env = options.env ?? process.env;
195
+ const cwd = options.cwd;
196
+ const helperPath = resolvePtyHelperPath(options.helperPath);
197
+
198
+ const child = spawn(helperPath, [command, ...commandArgs], {
199
+ cwd,
200
+ env,
201
+ stdio: ['pipe', 'pipe', 'pipe'],
202
+ });
203
+
204
+ const session = new PtySession(child);
205
+ if (
206
+ typeof options.initialCols === 'number' &&
207
+ Number.isFinite(options.initialCols) &&
208
+ options.initialCols > 0 &&
209
+ typeof options.initialRows === 'number' &&
210
+ Number.isFinite(options.initialRows) &&
211
+ options.initialRows > 0
212
+ ) {
213
+ session.resize(Math.floor(options.initialCols), Math.floor(options.initialRows));
214
+ }
215
+
216
+ return session;
217
+ }
@@ -0,0 +1,158 @@
1
+ import { startPtySession, type PtyExit } from './pty_host.ts';
2
+
3
+ interface BacklogEntry {
4
+ cursor: number;
5
+ chunk: Buffer;
6
+ }
7
+
8
+ export interface BrokerDataEvent {
9
+ cursor: number;
10
+ chunk: Buffer;
11
+ }
12
+
13
+ export interface BrokerAttachmentHandlers {
14
+ onData: (event: BrokerDataEvent) => void;
15
+ onExit: (exit: PtyExit) => void;
16
+ }
17
+
18
+ type StartPtySessionOptions = Parameters<typeof startPtySession>[0];
19
+ type StartSessionFactory = (options?: StartPtySessionOptions) => ReturnType<typeof startPtySession>;
20
+
21
+ interface StartSingleSessionBrokerDependencies {
22
+ startSession?: StartSessionFactory;
23
+ }
24
+
25
+ class SingleSessionBroker {
26
+ private readonly session: ReturnType<typeof startPtySession>;
27
+ private readonly maxBacklogBytes: number;
28
+ private readonly attachments = new Map<string, BrokerAttachmentHandlers>();
29
+ private readonly backlog: BacklogEntry[] = [];
30
+ private backlogBytes = 0;
31
+ private nextAttachmentId = 1;
32
+ private nextCursor = 1;
33
+ private latestExit: PtyExit | null = null;
34
+
35
+ constructor(
36
+ options?: StartPtySessionOptions,
37
+ maxBacklogBytes = 256 * 1024,
38
+ startSession: StartSessionFactory = startPtySession,
39
+ ) {
40
+ this.session = startSession(options);
41
+ this.maxBacklogBytes = maxBacklogBytes;
42
+
43
+ this.session.on('data', (chunk: Buffer) => {
44
+ this.handleData(chunk);
45
+ });
46
+
47
+ this.session.on('exit', (exit: unknown) => {
48
+ this.handleExit(exit as PtyExit);
49
+ });
50
+
51
+ this.session.on('error', (error: unknown) => {
52
+ this.handleError(error as Error);
53
+ });
54
+ }
55
+
56
+ attach(handlers: BrokerAttachmentHandlers, sinceCursor = 0): string {
57
+ const attachmentId = `attachment-${this.nextAttachmentId}`;
58
+ this.nextAttachmentId += 1;
59
+ this.attachments.set(attachmentId, handlers);
60
+
61
+ for (const entry of this.backlog) {
62
+ if (entry.cursor <= sinceCursor) {
63
+ continue;
64
+ }
65
+ handlers.onData({
66
+ cursor: entry.cursor,
67
+ chunk: Buffer.from(entry.chunk),
68
+ });
69
+ }
70
+
71
+ if (this.latestExit !== null) {
72
+ handlers.onExit(this.latestExit);
73
+ }
74
+
75
+ return attachmentId;
76
+ }
77
+
78
+ detach(attachmentId: string): void {
79
+ this.attachments.delete(attachmentId);
80
+ }
81
+
82
+ latestCursorValue(): number {
83
+ return this.nextCursor - 1;
84
+ }
85
+
86
+ write(data: string | Uint8Array): void {
87
+ this.session.write(data);
88
+ }
89
+
90
+ resize(cols: number, rows: number): void {
91
+ this.session.resize(cols, rows);
92
+ }
93
+
94
+ close(): void {
95
+ this.session.close();
96
+ }
97
+
98
+ processId(): number | null {
99
+ return this.session.processId();
100
+ }
101
+
102
+ private handleData(chunk: Buffer): void {
103
+ const fullChunk = Buffer.from(chunk);
104
+ let storedChunk = fullChunk;
105
+ if (storedChunk.length > this.maxBacklogBytes) {
106
+ storedChunk = storedChunk.subarray(storedChunk.length - this.maxBacklogBytes);
107
+ this.backlog.length = 0;
108
+ this.backlogBytes = 0;
109
+ }
110
+
111
+ const entry: BacklogEntry = {
112
+ cursor: this.nextCursor,
113
+ chunk: storedChunk,
114
+ };
115
+ this.nextCursor += 1;
116
+
117
+ this.backlog.push(entry);
118
+ this.backlogBytes += entry.chunk.length;
119
+ while (this.backlogBytes > this.maxBacklogBytes && this.backlog.length > 0) {
120
+ const removed = this.backlog.shift()!;
121
+ this.backlogBytes -= removed.chunk.length;
122
+ }
123
+
124
+ for (const handlers of this.attachments.values()) {
125
+ handlers.onData({
126
+ cursor: entry.cursor,
127
+ chunk: Buffer.from(fullChunk),
128
+ });
129
+ }
130
+ }
131
+
132
+ private handleExit(exit: PtyExit): void {
133
+ if (this.latestExit !== null) {
134
+ return;
135
+ }
136
+ this.latestExit = exit;
137
+ for (const handlers of this.attachments.values()) {
138
+ handlers.onExit(exit);
139
+ }
140
+ }
141
+
142
+ private handleError(error: Error): void {
143
+ void error;
144
+ this.handleExit({
145
+ code: null,
146
+ signal: null,
147
+ });
148
+ }
149
+ }
150
+
151
+ export function startSingleSessionBroker(
152
+ options?: StartPtySessionOptions,
153
+ maxBacklogBytes?: number,
154
+ dependencies: StartSingleSessionBrokerDependencies = {},
155
+ ): SingleSessionBroker {
156
+ const startSession = dependencies.startSession ?? startPtySession;
157
+ return new SingleSessionBroker(options, maxBacklogBytes, startSession);
158
+ }