@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,392 @@
1
+ import { connectControlPlaneStreamClient, type ControlPlaneStreamClient } from './stream-client.ts';
2
+ import type { ControlPlaneStreamServer } from './stream-server.ts';
3
+ import type {
4
+ StreamObservedEvent,
5
+ StreamSessionController,
6
+ StreamSessionKeyEventRecord,
7
+ StreamSessionRuntimeStatus,
8
+ StreamSessionStatusModel,
9
+ StreamTelemetrySummary,
10
+ } from './stream-protocol.ts';
11
+
12
+ interface BaseControlPlaneAddress {
13
+ host: string;
14
+ port: number;
15
+ authToken?: string;
16
+ connectRetryWindowMs?: number;
17
+ connectRetryDelayMs?: number;
18
+ }
19
+
20
+ interface EmbeddedControlPlaneOptions {
21
+ mode: 'embedded';
22
+ }
23
+
24
+ interface RemoteControlPlaneOptions extends BaseControlPlaneAddress {
25
+ mode: 'remote';
26
+ }
27
+
28
+ type CodexControlPlaneMode = EmbeddedControlPlaneOptions | RemoteControlPlaneOptions;
29
+
30
+ interface OpenCodexControlPlaneSessionOptions {
31
+ controlPlane: CodexControlPlaneMode;
32
+ sessionId: string;
33
+ args: string[];
34
+ env: Record<string, string>;
35
+ cwd?: string;
36
+ initialCols: number;
37
+ initialRows: number;
38
+ terminalForegroundHex?: string;
39
+ terminalBackgroundHex?: string;
40
+ }
41
+
42
+ interface OpenCodexControlPlaneSessionResult {
43
+ client: ControlPlaneStreamClient;
44
+ close: () => Promise<void>;
45
+ }
46
+
47
+ interface OpenCodexControlPlaneClientResult {
48
+ client: ControlPlaneStreamClient;
49
+ close: () => Promise<void>;
50
+ }
51
+
52
+ export type ControlPlaneKeyEvent =
53
+ | {
54
+ type: 'session-status';
55
+ sessionId: string;
56
+ status: StreamSessionRuntimeStatus;
57
+ attentionReason: string | null;
58
+ statusModel: StreamSessionStatusModel | null;
59
+ live: boolean;
60
+ ts: string;
61
+ directoryId: string | null;
62
+ conversationId: string | null;
63
+ telemetry: StreamTelemetrySummary | null;
64
+ controller: StreamSessionController | null;
65
+ cursor: number;
66
+ }
67
+ | {
68
+ type: 'session-telemetry';
69
+ sessionId: string;
70
+ keyEvent: StreamSessionKeyEventRecord;
71
+ ts: string;
72
+ directoryId: string | null;
73
+ conversationId: string | null;
74
+ cursor: number;
75
+ }
76
+ | {
77
+ type: 'session-control';
78
+ sessionId: string;
79
+ action: 'claimed' | 'released' | 'taken-over';
80
+ controller: StreamSessionController | null;
81
+ previousController: StreamSessionController | null;
82
+ reason: string | null;
83
+ ts: string;
84
+ directoryId: string | null;
85
+ conversationId: string | null;
86
+ cursor: number;
87
+ };
88
+
89
+ interface SubscribeControlPlaneKeyEventsOptions {
90
+ tenantId?: string;
91
+ userId?: string;
92
+ workspaceId?: string;
93
+ directoryId?: string;
94
+ conversationId?: string;
95
+ afterCursor?: number;
96
+ includeOutput?: boolean;
97
+ onEvent: (event: ControlPlaneKeyEvent) => void;
98
+ }
99
+
100
+ interface ControlPlaneKeyEventSubscription {
101
+ subscriptionId: string;
102
+ close: () => Promise<void>;
103
+ }
104
+
105
+ interface OpenCodexControlPlaneSessionDependencies {
106
+ startEmbeddedServer?: () => Promise<ControlPlaneStreamServer>;
107
+ }
108
+
109
+ function mapObservedEventToKeyEvent(
110
+ event: StreamObservedEvent,
111
+ cursor: number,
112
+ ): ControlPlaneKeyEvent | null {
113
+ if (event.type === 'session-status') {
114
+ return {
115
+ type: 'session-status',
116
+ sessionId: event.sessionId,
117
+ status: event.status,
118
+ attentionReason: event.attentionReason,
119
+ statusModel: event.statusModel,
120
+ live: event.live,
121
+ ts: event.ts,
122
+ directoryId: event.directoryId,
123
+ conversationId: event.conversationId,
124
+ telemetry: event.telemetry,
125
+ controller: event.controller,
126
+ cursor,
127
+ };
128
+ }
129
+ if (event.type === 'session-key-event') {
130
+ return {
131
+ type: 'session-telemetry',
132
+ sessionId: event.sessionId,
133
+ keyEvent: event.keyEvent,
134
+ ts: event.ts,
135
+ directoryId: event.directoryId,
136
+ conversationId: event.conversationId,
137
+ cursor,
138
+ };
139
+ }
140
+ if (event.type === 'session-control') {
141
+ return {
142
+ type: 'session-control',
143
+ sessionId: event.sessionId,
144
+ action: event.action,
145
+ controller: event.controller,
146
+ previousController: event.previousController,
147
+ reason: event.reason,
148
+ ts: event.ts,
149
+ directoryId: event.directoryId,
150
+ conversationId: event.conversationId,
151
+ cursor,
152
+ };
153
+ }
154
+ return null;
155
+ }
156
+
157
+ export async function subscribeControlPlaneKeyEvents(
158
+ client: ControlPlaneStreamClient,
159
+ options: SubscribeControlPlaneKeyEventsOptions,
160
+ ): Promise<ControlPlaneKeyEventSubscription> {
161
+ let subscriptionId: string | null = null;
162
+ const bufferedEnvelopes: Array<{
163
+ subscriptionId: string;
164
+ cursor: number;
165
+ event: StreamObservedEvent;
166
+ }> = [];
167
+
168
+ const emitIfRelevant = (payload: {
169
+ subscriptionId: string;
170
+ cursor: number;
171
+ event: StreamObservedEvent;
172
+ }): void => {
173
+ if (subscriptionId === null || payload.subscriptionId !== subscriptionId) {
174
+ return;
175
+ }
176
+ const mapped = mapObservedEventToKeyEvent(payload.event, payload.cursor);
177
+ if (mapped === null) {
178
+ return;
179
+ }
180
+ options.onEvent(mapped);
181
+ };
182
+
183
+ const removeListener = client.onEnvelope((envelope) => {
184
+ if (envelope.kind !== 'stream.event') {
185
+ return;
186
+ }
187
+ const payload = {
188
+ subscriptionId: envelope.subscriptionId,
189
+ cursor: envelope.cursor,
190
+ event: envelope.event,
191
+ };
192
+ if (subscriptionId === null) {
193
+ bufferedEnvelopes.push(payload);
194
+ return;
195
+ }
196
+ emitIfRelevant(payload);
197
+ });
198
+
199
+ const subscribeCommand: {
200
+ type: 'stream.subscribe';
201
+ tenantId?: string;
202
+ userId?: string;
203
+ workspaceId?: string;
204
+ directoryId?: string;
205
+ conversationId?: string;
206
+ includeOutput?: boolean;
207
+ afterCursor?: number;
208
+ } = {
209
+ type: 'stream.subscribe',
210
+ includeOutput: options.includeOutput ?? false,
211
+ };
212
+ if (options.tenantId !== undefined) {
213
+ subscribeCommand.tenantId = options.tenantId;
214
+ }
215
+ if (options.userId !== undefined) {
216
+ subscribeCommand.userId = options.userId;
217
+ }
218
+ if (options.workspaceId !== undefined) {
219
+ subscribeCommand.workspaceId = options.workspaceId;
220
+ }
221
+ if (options.directoryId !== undefined) {
222
+ subscribeCommand.directoryId = options.directoryId;
223
+ }
224
+ if (options.conversationId !== undefined) {
225
+ subscribeCommand.conversationId = options.conversationId;
226
+ }
227
+ if (options.afterCursor !== undefined) {
228
+ subscribeCommand.afterCursor = options.afterCursor;
229
+ }
230
+
231
+ let subscribed: Record<string, unknown>;
232
+ try {
233
+ subscribed = await client.sendCommand(subscribeCommand);
234
+ } catch (error: unknown) {
235
+ removeListener();
236
+ throw error;
237
+ }
238
+ const parsedSubscriptionId = subscribed['subscriptionId'];
239
+ if (typeof parsedSubscriptionId !== 'string' || parsedSubscriptionId.length === 0) {
240
+ removeListener();
241
+ throw new Error('control-plane stream.subscribe returned malformed subscription id');
242
+ }
243
+ subscriptionId = parsedSubscriptionId;
244
+ for (const payload of bufferedEnvelopes) {
245
+ emitIfRelevant(payload);
246
+ }
247
+ bufferedEnvelopes.length = 0;
248
+
249
+ let closed = false;
250
+ return {
251
+ subscriptionId: parsedSubscriptionId,
252
+ close: async () => {
253
+ if (closed) {
254
+ return;
255
+ }
256
+ closed = true;
257
+ removeListener();
258
+ try {
259
+ await client.sendCommand({
260
+ type: 'stream.unsubscribe',
261
+ subscriptionId: parsedSubscriptionId,
262
+ });
263
+ } catch {
264
+ // Best-effort unsubscribe on shutdown.
265
+ }
266
+ },
267
+ };
268
+ }
269
+
270
+ export async function openCodexControlPlaneClient(
271
+ controlPlane: CodexControlPlaneMode,
272
+ dependencies: OpenCodexControlPlaneSessionDependencies = {},
273
+ ): Promise<OpenCodexControlPlaneClientResult> {
274
+ let controlPlaneAddress: BaseControlPlaneAddress;
275
+ let embeddedServer: ControlPlaneStreamServer | null = null;
276
+ if (controlPlane.mode === 'embedded') {
277
+ const startEmbeddedServer = dependencies.startEmbeddedServer;
278
+ if (startEmbeddedServer === undefined) {
279
+ throw new Error('embedded mode requires a startEmbeddedServer dependency');
280
+ }
281
+ embeddedServer = await startEmbeddedServer();
282
+ const embeddedAddress = embeddedServer.address();
283
+ controlPlaneAddress = {
284
+ host: '127.0.0.1',
285
+ port: embeddedAddress.port,
286
+ };
287
+ } else {
288
+ controlPlaneAddress = controlPlane;
289
+ }
290
+
291
+ const clientConnectOptions: {
292
+ host: string;
293
+ port: number;
294
+ authToken?: string;
295
+ connectRetryWindowMs?: number;
296
+ connectRetryDelayMs?: number;
297
+ } = {
298
+ host: controlPlaneAddress.host,
299
+ port: controlPlaneAddress.port,
300
+ };
301
+ if (controlPlaneAddress.authToken !== undefined) {
302
+ clientConnectOptions.authToken = controlPlaneAddress.authToken;
303
+ }
304
+ if (controlPlaneAddress.connectRetryWindowMs !== undefined) {
305
+ clientConnectOptions.connectRetryWindowMs = controlPlaneAddress.connectRetryWindowMs;
306
+ }
307
+ if (controlPlaneAddress.connectRetryDelayMs !== undefined) {
308
+ clientConnectOptions.connectRetryDelayMs = controlPlaneAddress.connectRetryDelayMs;
309
+ }
310
+ const client = await connectControlPlaneStreamClient(clientConnectOptions);
311
+
312
+ return {
313
+ client,
314
+ close: async () => {
315
+ client.close();
316
+ if (embeddedServer !== null) {
317
+ await embeddedServer.close();
318
+ }
319
+ },
320
+ };
321
+ }
322
+
323
+ export async function openCodexControlPlaneSession(
324
+ options: OpenCodexControlPlaneSessionOptions,
325
+ dependencies: OpenCodexControlPlaneSessionDependencies = {},
326
+ ): Promise<OpenCodexControlPlaneSessionResult> {
327
+ const opened = await openCodexControlPlaneClient(options.controlPlane, dependencies);
328
+ const client = opened.client;
329
+
330
+ try {
331
+ const startCommand: {
332
+ type: 'pty.start';
333
+ sessionId: string;
334
+ args: string[];
335
+ env: Record<string, string>;
336
+ cwd?: string;
337
+ initialCols: number;
338
+ initialRows: number;
339
+ terminalForegroundHex?: string;
340
+ terminalBackgroundHex?: string;
341
+ } = {
342
+ type: 'pty.start',
343
+ sessionId: options.sessionId,
344
+ args: options.args,
345
+ env: options.env,
346
+ initialCols: options.initialCols,
347
+ initialRows: options.initialRows,
348
+ };
349
+ if (options.cwd !== undefined) {
350
+ startCommand.cwd = options.cwd;
351
+ }
352
+ if (options.terminalForegroundHex !== undefined) {
353
+ startCommand.terminalForegroundHex = options.terminalForegroundHex;
354
+ }
355
+ if (options.terminalBackgroundHex !== undefined) {
356
+ startCommand.terminalBackgroundHex = options.terminalBackgroundHex;
357
+ }
358
+
359
+ const startResult = await client.sendCommand(startCommand);
360
+ if (startResult['sessionId'] !== options.sessionId) {
361
+ throw new Error('control-plane pty.start returned unexpected session id');
362
+ }
363
+
364
+ await client.sendCommand({
365
+ type: 'pty.subscribe-events',
366
+ sessionId: options.sessionId,
367
+ });
368
+ await client.sendCommand({
369
+ type: 'pty.attach',
370
+ sessionId: options.sessionId,
371
+ sinceCursor: 0,
372
+ });
373
+ } catch (error: unknown) {
374
+ await opened.close();
375
+ throw error;
376
+ }
377
+
378
+ return {
379
+ client,
380
+ close: async () => {
381
+ try {
382
+ await client.sendCommand({
383
+ type: 'pty.close',
384
+ sessionId: options.sessionId,
385
+ });
386
+ } catch {
387
+ // Best-effort close only.
388
+ }
389
+ await opened.close();
390
+ },
391
+ };
392
+ }