@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,567 @@
1
+ import type {
2
+ StreamCommand,
3
+ StreamSessionControllerType,
4
+ StreamSessionListSort,
5
+ } from '../control-plane/stream-protocol.ts';
6
+ import {
7
+ parseSessionSummaryList,
8
+ parseSessionSummaryRecord,
9
+ } from '../control-plane/session-summary.ts';
10
+ import {
11
+ parseConversationRecord,
12
+ parseDirectoryGitStatusRecord,
13
+ parseDirectoryRecord,
14
+ parseRepositoryRecord,
15
+ parseSessionControllerRecord,
16
+ parseTaskRecord,
17
+ } from '../mux/live-mux/control-plane-records.ts';
18
+
19
+ interface ControlPlaneScope {
20
+ readonly tenantId: string;
21
+ readonly userId: string;
22
+ readonly workspaceId: string;
23
+ }
24
+
25
+ interface ControlPlaneCommandClient {
26
+ sendCommand(command: StreamCommand): Promise<Record<string, unknown>>;
27
+ }
28
+
29
+ type ControlPlaneRepositoryRecord = NonNullable<ReturnType<typeof parseRepositoryRecord>>;
30
+ type ControlPlaneTaskRecord = NonNullable<ReturnType<typeof parseTaskRecord>>;
31
+ type ControlPlaneDirectoryRecord = NonNullable<ReturnType<typeof parseDirectoryRecord>>;
32
+ type ControlPlaneConversationRecord = NonNullable<ReturnType<typeof parseConversationRecord>>;
33
+ type ControlPlaneDirectoryGitStatusRecord = NonNullable<
34
+ ReturnType<typeof parseDirectoryGitStatusRecord>
35
+ >;
36
+ type ControlPlaneSessionControllerRecord = NonNullable<
37
+ ReturnType<typeof parseSessionControllerRecord>
38
+ >;
39
+ type ControlPlaneSessionSummary = NonNullable<ReturnType<typeof parseSessionSummaryRecord>>;
40
+
41
+ export class ControlPlaneService {
42
+ constructor(
43
+ private readonly client: ControlPlaneCommandClient,
44
+ private readonly scope: ControlPlaneScope,
45
+ ) {}
46
+
47
+ async listRepositories(): Promise<readonly ControlPlaneRepositoryRecord[]> {
48
+ const result = await this.client.sendCommand({
49
+ type: 'repository.list',
50
+ tenantId: this.scope.tenantId,
51
+ userId: this.scope.userId,
52
+ workspaceId: this.scope.workspaceId,
53
+ });
54
+ const rawRepositories = result['repositories'];
55
+ if (!Array.isArray(rawRepositories)) {
56
+ throw new Error('control-plane repository.list returned malformed repositories');
57
+ }
58
+ const repositories: ControlPlaneRepositoryRecord[] = [];
59
+ for (const value of rawRepositories) {
60
+ const parsed = parseRepositoryRecord(value);
61
+ if (parsed === null) {
62
+ throw new Error('control-plane repository.list returned malformed repository record');
63
+ }
64
+ repositories.push(parsed);
65
+ }
66
+ return repositories;
67
+ }
68
+
69
+ async upsertRepository(input: {
70
+ repositoryId?: string;
71
+ name: string;
72
+ remoteUrl: string;
73
+ defaultBranch?: string;
74
+ metadata?: Record<string, unknown>;
75
+ }): Promise<ControlPlaneRepositoryRecord> {
76
+ const command: StreamCommand = {
77
+ type: 'repository.upsert',
78
+ tenantId: this.scope.tenantId,
79
+ userId: this.scope.userId,
80
+ workspaceId: this.scope.workspaceId,
81
+ name: input.name,
82
+ remoteUrl: input.remoteUrl,
83
+ };
84
+ if (input.repositoryId !== undefined) {
85
+ command.repositoryId = input.repositoryId;
86
+ }
87
+ if (input.defaultBranch !== undefined) {
88
+ command.defaultBranch = input.defaultBranch;
89
+ }
90
+ if (input.metadata !== undefined) {
91
+ command.metadata = input.metadata;
92
+ }
93
+ const result = await this.client.sendCommand(command);
94
+ const parsed = parseRepositoryRecord(result['repository']);
95
+ if (parsed === null) {
96
+ throw new Error('control-plane repository.upsert returned malformed repository record');
97
+ }
98
+ return parsed;
99
+ }
100
+
101
+ async updateRepository(input: {
102
+ repositoryId: string;
103
+ name?: string;
104
+ remoteUrl?: string;
105
+ defaultBranch?: string;
106
+ metadata?: Record<string, unknown>;
107
+ }): Promise<ControlPlaneRepositoryRecord> {
108
+ const command: StreamCommand = {
109
+ type: 'repository.update',
110
+ repositoryId: input.repositoryId,
111
+ };
112
+ if (input.name !== undefined) {
113
+ command.name = input.name;
114
+ }
115
+ if (input.remoteUrl !== undefined) {
116
+ command.remoteUrl = input.remoteUrl;
117
+ }
118
+ if (input.defaultBranch !== undefined) {
119
+ command.defaultBranch = input.defaultBranch;
120
+ }
121
+ if (input.metadata !== undefined) {
122
+ command.metadata = input.metadata;
123
+ }
124
+ const result = await this.client.sendCommand(command);
125
+ const parsed = parseRepositoryRecord(result['repository']);
126
+ if (parsed === null) {
127
+ throw new Error('control-plane repository.update returned malformed repository record');
128
+ }
129
+ return parsed;
130
+ }
131
+
132
+ async archiveRepository(repositoryId: string): Promise<void> {
133
+ await this.client.sendCommand({
134
+ type: 'repository.archive',
135
+ repositoryId,
136
+ });
137
+ }
138
+
139
+ async upsertDirectory(input: {
140
+ directoryId: string;
141
+ path: string;
142
+ }): Promise<ControlPlaneDirectoryRecord> {
143
+ const result = await this.client.sendCommand({
144
+ type: 'directory.upsert',
145
+ directoryId: input.directoryId,
146
+ tenantId: this.scope.tenantId,
147
+ userId: this.scope.userId,
148
+ workspaceId: this.scope.workspaceId,
149
+ path: input.path,
150
+ });
151
+ const parsed = parseDirectoryRecord(result['directory']);
152
+ if (parsed === null) {
153
+ throw new Error('control-plane directory.upsert returned malformed directory record');
154
+ }
155
+ return parsed;
156
+ }
157
+
158
+ async listDirectories(): Promise<readonly ControlPlaneDirectoryRecord[]> {
159
+ const result = await this.client.sendCommand({
160
+ type: 'directory.list',
161
+ tenantId: this.scope.tenantId,
162
+ userId: this.scope.userId,
163
+ workspaceId: this.scope.workspaceId,
164
+ });
165
+ const rows = Array.isArray(result['directories']) ? result['directories'] : [];
166
+ const directories: ControlPlaneDirectoryRecord[] = [];
167
+ for (const row of rows) {
168
+ const parsed = parseDirectoryRecord(row);
169
+ if (parsed !== null) {
170
+ directories.push(parsed);
171
+ }
172
+ }
173
+ return directories;
174
+ }
175
+
176
+ async listDirectoryGitStatuses(input?: {
177
+ directoryId?: string;
178
+ }): Promise<readonly ControlPlaneDirectoryGitStatusRecord[]> {
179
+ const command: StreamCommand = {
180
+ type: 'directory.git-status',
181
+ tenantId: this.scope.tenantId,
182
+ userId: this.scope.userId,
183
+ workspaceId: this.scope.workspaceId,
184
+ };
185
+ if (input?.directoryId !== undefined) {
186
+ command.directoryId = input.directoryId;
187
+ }
188
+ const result = await this.client.sendCommand(command);
189
+ const rows = Array.isArray(result['gitStatuses']) ? result['gitStatuses'] : [];
190
+ const statuses: ControlPlaneDirectoryGitStatusRecord[] = [];
191
+ for (const row of rows) {
192
+ const parsed = parseDirectoryGitStatusRecord(row);
193
+ if (parsed !== null) {
194
+ statuses.push(parsed);
195
+ }
196
+ }
197
+ return statuses;
198
+ }
199
+
200
+ async listConversations(directoryId: string): Promise<readonly ControlPlaneConversationRecord[]> {
201
+ const result = await this.client.sendCommand({
202
+ type: 'conversation.list',
203
+ directoryId,
204
+ tenantId: this.scope.tenantId,
205
+ userId: this.scope.userId,
206
+ workspaceId: this.scope.workspaceId,
207
+ });
208
+ const rows = Array.isArray(result['conversations']) ? result['conversations'] : [];
209
+ const conversations: ControlPlaneConversationRecord[] = [];
210
+ for (const row of rows) {
211
+ const parsed = parseConversationRecord(row);
212
+ if (parsed !== null) {
213
+ conversations.push(parsed);
214
+ }
215
+ }
216
+ return conversations;
217
+ }
218
+
219
+ async createConversation(input: {
220
+ conversationId: string;
221
+ directoryId: string;
222
+ title: string;
223
+ agentType: string;
224
+ adapterState: Record<string, unknown>;
225
+ }): Promise<void> {
226
+ await this.client.sendCommand({
227
+ type: 'conversation.create',
228
+ conversationId: input.conversationId,
229
+ directoryId: input.directoryId,
230
+ title: input.title,
231
+ agentType: input.agentType,
232
+ adapterState: input.adapterState,
233
+ });
234
+ }
235
+
236
+ async updateConversationTitle(input: {
237
+ conversationId: string;
238
+ title: string;
239
+ }): Promise<ControlPlaneConversationRecord | null> {
240
+ const result = await this.client.sendCommand({
241
+ type: 'conversation.update',
242
+ conversationId: input.conversationId,
243
+ title: input.title,
244
+ });
245
+ return parseConversationRecord(result['conversation']);
246
+ }
247
+
248
+ async archiveConversation(conversationId: string): Promise<void> {
249
+ await this.client.sendCommand({
250
+ type: 'conversation.archive',
251
+ conversationId,
252
+ });
253
+ }
254
+
255
+ async archiveDirectory(directoryId: string): Promise<void> {
256
+ await this.client.sendCommand({
257
+ type: 'directory.archive',
258
+ directoryId,
259
+ });
260
+ }
261
+
262
+ async attachPty(input: { sessionId: string; sinceCursor: number }): Promise<void> {
263
+ await this.client.sendCommand({
264
+ type: 'pty.attach',
265
+ sessionId: input.sessionId,
266
+ sinceCursor: input.sinceCursor,
267
+ });
268
+ }
269
+
270
+ async detachPty(sessionId: string): Promise<void> {
271
+ await this.client.sendCommand({
272
+ type: 'pty.detach',
273
+ sessionId,
274
+ });
275
+ }
276
+
277
+ async subscribePtyEvents(sessionId: string): Promise<void> {
278
+ await this.client.sendCommand({
279
+ type: 'pty.subscribe-events',
280
+ sessionId,
281
+ });
282
+ }
283
+
284
+ async unsubscribePtyEvents(sessionId: string): Promise<void> {
285
+ await this.client.sendCommand({
286
+ type: 'pty.unsubscribe-events',
287
+ sessionId,
288
+ });
289
+ }
290
+
291
+ async startPtySession(input: {
292
+ sessionId: string;
293
+ args: readonly string[];
294
+ env?: Record<string, string>;
295
+ cwd?: string;
296
+ initialCols: number;
297
+ initialRows: number;
298
+ terminalForegroundHex?: string;
299
+ terminalBackgroundHex?: string;
300
+ worktreeId?: string;
301
+ }): Promise<void> {
302
+ const command: StreamCommand = {
303
+ type: 'pty.start',
304
+ sessionId: input.sessionId,
305
+ args: [...input.args],
306
+ initialCols: input.initialCols,
307
+ initialRows: input.initialRows,
308
+ tenantId: this.scope.tenantId,
309
+ userId: this.scope.userId,
310
+ workspaceId: this.scope.workspaceId,
311
+ };
312
+ if (input.env !== undefined) {
313
+ command.env = input.env;
314
+ }
315
+ if (input.cwd !== undefined) {
316
+ command.cwd = input.cwd;
317
+ }
318
+ if (input.terminalForegroundHex !== undefined) {
319
+ command.terminalForegroundHex = input.terminalForegroundHex;
320
+ }
321
+ if (input.terminalBackgroundHex !== undefined) {
322
+ command.terminalBackgroundHex = input.terminalBackgroundHex;
323
+ }
324
+ if (input.worktreeId !== undefined) {
325
+ command.worktreeId = input.worktreeId;
326
+ }
327
+ await this.client.sendCommand(command);
328
+ }
329
+
330
+ async closePtySession(sessionId: string): Promise<void> {
331
+ await this.client.sendCommand({
332
+ type: 'pty.close',
333
+ sessionId,
334
+ });
335
+ }
336
+
337
+ async getSessionStatus(sessionId: string): Promise<ControlPlaneSessionSummary | null> {
338
+ const result = await this.client.sendCommand({
339
+ type: 'session.status',
340
+ sessionId,
341
+ });
342
+ return parseSessionSummaryRecord(result);
343
+ }
344
+
345
+ async listSessions(input?: {
346
+ sort?: StreamSessionListSort;
347
+ worktreeId?: string;
348
+ }): Promise<readonly ControlPlaneSessionSummary[]> {
349
+ const command: StreamCommand = {
350
+ type: 'session.list',
351
+ tenantId: this.scope.tenantId,
352
+ userId: this.scope.userId,
353
+ workspaceId: this.scope.workspaceId,
354
+ };
355
+ if (input?.sort !== undefined) {
356
+ command.sort = input.sort;
357
+ }
358
+ if (input?.worktreeId !== undefined) {
359
+ command.worktreeId = input.worktreeId;
360
+ }
361
+ const result = await this.client.sendCommand(command);
362
+ return parseSessionSummaryList(result['sessions']);
363
+ }
364
+
365
+ async removeSession(sessionId: string): Promise<void> {
366
+ await this.client.sendCommand({
367
+ type: 'session.remove',
368
+ sessionId,
369
+ });
370
+ }
371
+
372
+ async claimSession(input: {
373
+ sessionId: string;
374
+ controllerId: string;
375
+ controllerType: StreamSessionControllerType;
376
+ controllerLabel: string;
377
+ reason: string;
378
+ takeover: boolean;
379
+ }): Promise<ControlPlaneSessionControllerRecord | null> {
380
+ const result = await this.client.sendCommand({
381
+ type: 'session.claim',
382
+ sessionId: input.sessionId,
383
+ controllerId: input.controllerId,
384
+ controllerType: input.controllerType,
385
+ controllerLabel: input.controllerLabel,
386
+ reason: input.reason,
387
+ takeover: input.takeover,
388
+ });
389
+ return parseSessionControllerRecord(result['controller']);
390
+ }
391
+
392
+ async respondToSession(
393
+ sessionId: string,
394
+ text: string,
395
+ ): Promise<{ responded: boolean; sentBytes: number }> {
396
+ const result = await this.client.sendCommand({
397
+ type: 'session.respond',
398
+ sessionId,
399
+ text,
400
+ });
401
+ const responded = result['responded'];
402
+ const sentBytes = result['sentBytes'];
403
+ if (typeof responded !== 'boolean' || typeof sentBytes !== 'number') {
404
+ throw new Error('control-plane session.respond returned malformed response');
405
+ }
406
+ return {
407
+ responded,
408
+ sentBytes,
409
+ };
410
+ }
411
+
412
+ async interruptSession(sessionId: string): Promise<{ interrupted: boolean }> {
413
+ const result = await this.client.sendCommand({
414
+ type: 'session.interrupt',
415
+ sessionId,
416
+ });
417
+ const interrupted = result['interrupted'];
418
+ if (typeof interrupted !== 'boolean') {
419
+ throw new Error('control-plane session.interrupt returned malformed response');
420
+ }
421
+ return {
422
+ interrupted,
423
+ };
424
+ }
425
+
426
+ async listTasks(limit = 1000): Promise<readonly ControlPlaneTaskRecord[]> {
427
+ const result = await this.client.sendCommand({
428
+ type: 'task.list',
429
+ tenantId: this.scope.tenantId,
430
+ userId: this.scope.userId,
431
+ workspaceId: this.scope.workspaceId,
432
+ limit,
433
+ });
434
+ return this.parseTaskListFromResult(
435
+ result,
436
+ 'control-plane task.list returned malformed tasks',
437
+ 'control-plane task.list returned malformed task record',
438
+ );
439
+ }
440
+
441
+ async createTask(input: {
442
+ repositoryId: string;
443
+ title: string;
444
+ description: string;
445
+ }): Promise<ControlPlaneTaskRecord> {
446
+ const result = await this.client.sendCommand({
447
+ type: 'task.create',
448
+ tenantId: this.scope.tenantId,
449
+ userId: this.scope.userId,
450
+ workspaceId: this.scope.workspaceId,
451
+ repositoryId: input.repositoryId,
452
+ title: input.title,
453
+ description: input.description,
454
+ });
455
+ return this.parseTaskFromResult(
456
+ result,
457
+ 'control-plane task.create returned malformed task record',
458
+ );
459
+ }
460
+
461
+ async updateTask(input: {
462
+ taskId: string;
463
+ repositoryId: string | null;
464
+ title: string;
465
+ description: string;
466
+ }): Promise<ControlPlaneTaskRecord> {
467
+ const result = await this.client.sendCommand({
468
+ type: 'task.update',
469
+ taskId: input.taskId,
470
+ repositoryId: input.repositoryId,
471
+ title: input.title,
472
+ description: input.description,
473
+ });
474
+ return this.parseTaskFromResult(
475
+ result,
476
+ 'control-plane task.update returned malformed task record',
477
+ );
478
+ }
479
+
480
+ async taskReady(taskId: string): Promise<ControlPlaneTaskRecord> {
481
+ const result = await this.client.sendCommand({
482
+ type: 'task.ready',
483
+ taskId,
484
+ });
485
+ return this.parseTaskFromResult(
486
+ result,
487
+ 'control-plane task.ready returned malformed task record',
488
+ );
489
+ }
490
+
491
+ async taskDraft(taskId: string): Promise<ControlPlaneTaskRecord> {
492
+ const result = await this.client.sendCommand({
493
+ type: 'task.draft',
494
+ taskId,
495
+ });
496
+ return this.parseTaskFromResult(
497
+ result,
498
+ 'control-plane task.draft returned malformed task record',
499
+ );
500
+ }
501
+
502
+ async taskComplete(taskId: string): Promise<ControlPlaneTaskRecord> {
503
+ const result = await this.client.sendCommand({
504
+ type: 'task.complete',
505
+ taskId,
506
+ });
507
+ return this.parseTaskFromResult(
508
+ result,
509
+ 'control-plane task.complete returned malformed task record',
510
+ );
511
+ }
512
+
513
+ async reorderTasks(
514
+ orderedTaskIds: readonly string[],
515
+ ): Promise<readonly ControlPlaneTaskRecord[]> {
516
+ const result = await this.client.sendCommand({
517
+ type: 'task.reorder',
518
+ tenantId: this.scope.tenantId,
519
+ userId: this.scope.userId,
520
+ workspaceId: this.scope.workspaceId,
521
+ orderedTaskIds: [...orderedTaskIds],
522
+ });
523
+ return this.parseTaskListFromResult(
524
+ result,
525
+ 'control-plane task.reorder returned malformed tasks',
526
+ 'control-plane task.reorder returned malformed task record',
527
+ );
528
+ }
529
+
530
+ async deleteTask(taskId: string): Promise<void> {
531
+ await this.client.sendCommand({
532
+ type: 'task.delete',
533
+ taskId,
534
+ });
535
+ }
536
+
537
+ private parseTaskFromResult(
538
+ result: Record<string, unknown>,
539
+ malformedTaskError: string,
540
+ ): ControlPlaneTaskRecord {
541
+ const parsed = parseTaskRecord(result['task']);
542
+ if (parsed === null) {
543
+ throw new Error(malformedTaskError);
544
+ }
545
+ return parsed;
546
+ }
547
+
548
+ private parseTaskListFromResult(
549
+ result: Record<string, unknown>,
550
+ malformedListError: string,
551
+ malformedRecordError: string,
552
+ ): readonly ControlPlaneTaskRecord[] {
553
+ const rawTasks = result['tasks'];
554
+ if (!Array.isArray(rawTasks)) {
555
+ throw new Error(malformedListError);
556
+ }
557
+ const tasks: ControlPlaneTaskRecord[] = [];
558
+ for (const value of rawTasks) {
559
+ const parsed = parseTaskRecord(value);
560
+ if (parsed === null) {
561
+ throw new Error(malformedRecordError);
562
+ }
563
+ tasks.push(parsed);
564
+ }
565
+ return tasks;
566
+ }
567
+ }