@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,380 @@
1
+ import type { PtyExit } from '../pty/pty_host.ts';
2
+ import type {
3
+ StreamSessionController,
4
+ StreamSessionControllerType,
5
+ StreamSessionRuntimeStatus,
6
+ StreamSessionStatusModel,
7
+ StreamTelemetrySummary,
8
+ } from './stream-protocol.ts';
9
+
10
+ interface StreamSessionSummary {
11
+ readonly sessionId: string;
12
+ readonly directoryId: string | null;
13
+ readonly tenantId: string;
14
+ readonly userId: string;
15
+ readonly workspaceId: string;
16
+ readonly worktreeId: string;
17
+ readonly status: StreamSessionRuntimeStatus;
18
+ readonly attentionReason: string | null;
19
+ readonly statusModel: StreamSessionStatusModel | null;
20
+ readonly latestCursor: number | null;
21
+ readonly processId: number | null;
22
+ readonly attachedClients: number;
23
+ readonly eventSubscribers: number;
24
+ readonly startedAt: string;
25
+ readonly lastEventAt: string | null;
26
+ readonly lastExit: PtyExit | null;
27
+ readonly exitedAt: string | null;
28
+ readonly live: boolean;
29
+ readonly launchCommand: string | null;
30
+ readonly telemetry: StreamTelemetrySummary | null;
31
+ readonly controller: StreamSessionController | null;
32
+ }
33
+
34
+ function asRecord(value: unknown): Record<string, unknown> | null {
35
+ if (typeof value !== 'object' || value === null) {
36
+ return null;
37
+ }
38
+ return value as Record<string, unknown>;
39
+ }
40
+
41
+ function readString(value: unknown): string | null {
42
+ return typeof value === 'string' ? value : null;
43
+ }
44
+
45
+ function readNullableString(value: unknown): string | null | undefined {
46
+ if (value === undefined) {
47
+ return undefined;
48
+ }
49
+ if (value === null) {
50
+ return null;
51
+ }
52
+ if (typeof value === 'string') {
53
+ return value;
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ function readNullableNumber(value: unknown): number | null | undefined {
59
+ if (value === undefined) {
60
+ return undefined;
61
+ }
62
+ if (value === null) {
63
+ return null;
64
+ }
65
+ if (typeof value === 'number' && Number.isFinite(value)) {
66
+ return value;
67
+ }
68
+ return undefined;
69
+ }
70
+
71
+ function readBoolean(value: unknown): boolean | null {
72
+ return typeof value === 'boolean' ? value : null;
73
+ }
74
+
75
+ function readNumber(value: unknown): number | null {
76
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
77
+ }
78
+
79
+ function readExit(value: unknown): PtyExit | null | undefined {
80
+ if (value === undefined) {
81
+ return undefined;
82
+ }
83
+ if (value === null) {
84
+ return null;
85
+ }
86
+ const record = asRecord(value);
87
+ if (record === null) {
88
+ return undefined;
89
+ }
90
+ const code = readNullableNumber(record['code']);
91
+ const signal = readNullableString(record['signal']);
92
+ if (code === undefined || signal === undefined) {
93
+ return undefined;
94
+ }
95
+ if (signal !== null && !/^SIG[A-Z0-9]+(?:_[A-Z0-9]+)*$/.test(signal)) {
96
+ return undefined;
97
+ }
98
+ return {
99
+ code,
100
+ signal: signal as NodeJS.Signals | null,
101
+ };
102
+ }
103
+
104
+ function readTelemetrySource(
105
+ value: unknown,
106
+ ): 'otlp-log' | 'otlp-metric' | 'otlp-trace' | 'history' | null {
107
+ if (
108
+ value === 'otlp-log' ||
109
+ value === 'otlp-metric' ||
110
+ value === 'otlp-trace' ||
111
+ value === 'history'
112
+ ) {
113
+ return value;
114
+ }
115
+ return null;
116
+ }
117
+
118
+ function readTelemetrySummary(value: unknown): StreamTelemetrySummary | null | undefined {
119
+ if (value === undefined) {
120
+ return undefined;
121
+ }
122
+ if (value === null) {
123
+ return null;
124
+ }
125
+ const record = asRecord(value);
126
+ if (record === null) {
127
+ return undefined;
128
+ }
129
+ const source = readTelemetrySource(record['source']);
130
+ const eventName = readNullableString(record['eventName']);
131
+ const severity = readNullableString(record['severity']);
132
+ const summary = readNullableString(record['summary']);
133
+ const observedAt = readString(record['observedAt']);
134
+ if (
135
+ source === null ||
136
+ eventName === undefined ||
137
+ severity === undefined ||
138
+ summary === undefined ||
139
+ observedAt === null
140
+ ) {
141
+ return undefined;
142
+ }
143
+ return {
144
+ source,
145
+ eventName,
146
+ severity,
147
+ summary,
148
+ observedAt,
149
+ };
150
+ }
151
+
152
+ function readSessionControllerType(value: unknown): StreamSessionControllerType | null {
153
+ if (value === 'human' || value === 'agent' || value === 'automation') {
154
+ return value;
155
+ }
156
+ return null;
157
+ }
158
+
159
+ function readSessionController(value: unknown): StreamSessionController | null | undefined {
160
+ if (value === undefined) {
161
+ return undefined;
162
+ }
163
+ if (value === null) {
164
+ return null;
165
+ }
166
+ const record = asRecord(value);
167
+ if (record === null) {
168
+ return undefined;
169
+ }
170
+ const controllerId = readString(record['controllerId']);
171
+ const controllerType = readSessionControllerType(record['controllerType']);
172
+ const controllerLabel = readNullableString(record['controllerLabel']);
173
+ const claimedAt = readString(record['claimedAt']);
174
+ if (
175
+ controllerId === null ||
176
+ controllerType === null ||
177
+ controllerLabel === undefined ||
178
+ claimedAt === null
179
+ ) {
180
+ return undefined;
181
+ }
182
+ return {
183
+ controllerId,
184
+ controllerType,
185
+ controllerLabel,
186
+ claimedAt,
187
+ };
188
+ }
189
+
190
+ function readSessionStatusModel(value: unknown): StreamSessionStatusModel | null | undefined {
191
+ if (value === undefined) {
192
+ return undefined;
193
+ }
194
+ if (value === null) {
195
+ return null;
196
+ }
197
+ const record = asRecord(value);
198
+ if (record === null) {
199
+ return undefined;
200
+ }
201
+ const runtimeStatus = readString(record['runtimeStatus']);
202
+ const phase = readString(record['phase']);
203
+ const glyph = readString(record['glyph']);
204
+ const badge = readString(record['badge']);
205
+ const detailText = readString(record['detailText']);
206
+ const attentionReason = readNullableString(record['attentionReason']);
207
+ const lastKnownWork = readNullableString(record['lastKnownWork']);
208
+ const lastKnownWorkAt = readNullableString(record['lastKnownWorkAt']);
209
+ const phaseHintRaw = readNullableString(record['phaseHint']);
210
+ const observedAt = readString(record['observedAt']);
211
+ if (
212
+ runtimeStatus === null ||
213
+ !isRuntimeStatus(runtimeStatus) ||
214
+ phase === null ||
215
+ (phase !== 'needs-action' &&
216
+ phase !== 'starting' &&
217
+ phase !== 'working' &&
218
+ phase !== 'idle' &&
219
+ phase !== 'exited') ||
220
+ glyph === null ||
221
+ (glyph !== '▲' && glyph !== '◔' && glyph !== '◆' && glyph !== '○' && glyph !== '■') ||
222
+ badge === null ||
223
+ (badge !== 'NEED' && badge !== 'RUN ' && badge !== 'DONE' && badge !== 'EXIT') ||
224
+ detailText === null ||
225
+ attentionReason === undefined ||
226
+ lastKnownWork === undefined ||
227
+ lastKnownWorkAt === undefined ||
228
+ phaseHintRaw === undefined ||
229
+ observedAt === null
230
+ ) {
231
+ return undefined;
232
+ }
233
+ const phaseHint =
234
+ phaseHintRaw === null ||
235
+ phaseHintRaw === 'needs-action' ||
236
+ phaseHintRaw === 'working' ||
237
+ phaseHintRaw === 'idle'
238
+ ? phaseHintRaw
239
+ : undefined;
240
+ if (phaseHint === undefined) {
241
+ return undefined;
242
+ }
243
+ return {
244
+ runtimeStatus,
245
+ phase,
246
+ glyph,
247
+ badge,
248
+ detailText,
249
+ attentionReason,
250
+ lastKnownWork,
251
+ lastKnownWorkAt,
252
+ phaseHint,
253
+ observedAt,
254
+ };
255
+ }
256
+
257
+ function isRuntimeStatus(value: string): value is StreamSessionRuntimeStatus {
258
+ return (
259
+ value === 'running' || value === 'needs-input' || value === 'completed' || value === 'exited'
260
+ );
261
+ }
262
+
263
+ export function parseSessionSummaryRecord(value: unknown): StreamSessionSummary | null {
264
+ const record = asRecord(value);
265
+ if (record === null) {
266
+ return null;
267
+ }
268
+ const sessionId = readString(record['sessionId']);
269
+ const directoryId = readNullableString(record['directoryId']);
270
+ const tenantId = readString(record['tenantId']);
271
+ const userId = readString(record['userId']);
272
+ const workspaceId = readString(record['workspaceId']);
273
+ const worktreeId = readString(record['worktreeId']);
274
+ const status = readString(record['status']);
275
+ if (
276
+ sessionId === null ||
277
+ directoryId === undefined ||
278
+ tenantId === null ||
279
+ userId === null ||
280
+ workspaceId === null ||
281
+ worktreeId === null ||
282
+ status === null ||
283
+ !isRuntimeStatus(status)
284
+ ) {
285
+ return null;
286
+ }
287
+ const attentionReason = readNullableString(record['attentionReason']);
288
+ const statusModel = readSessionStatusModel(record['statusModel']);
289
+ const latestCursor = readNullableNumber(record['latestCursor']);
290
+ const processId = readNullableNumber(record['processId']);
291
+ const attachedClients = readNumber(record['attachedClients']);
292
+ const eventSubscribers = readNumber(record['eventSubscribers']);
293
+ const startedAt = readString(record['startedAt']);
294
+ const lastEventAt = readNullableString(record['lastEventAt']);
295
+ const lastExit = readExit(record['lastExit']);
296
+ const exitedAt = readNullableString(record['exitedAt']);
297
+ const live = readBoolean(record['live']);
298
+ const launchCommand = readNullableString(record['launchCommand']);
299
+ const telemetry = readTelemetrySummary(record['telemetry']);
300
+ const controller = readSessionController(record['controller']);
301
+ if (attentionReason === undefined) {
302
+ return null;
303
+ }
304
+ if (statusModel === undefined) {
305
+ return null;
306
+ }
307
+ if (latestCursor === undefined) {
308
+ return null;
309
+ }
310
+ if (processId === undefined) {
311
+ return null;
312
+ }
313
+ if (attachedClients === null) {
314
+ return null;
315
+ }
316
+ if (eventSubscribers === null) {
317
+ return null;
318
+ }
319
+ if (startedAt === null) {
320
+ return null;
321
+ }
322
+ if (lastEventAt === undefined) {
323
+ return null;
324
+ }
325
+ if (lastExit === undefined) {
326
+ return null;
327
+ }
328
+ if (exitedAt === undefined) {
329
+ return null;
330
+ }
331
+ if (live === null) {
332
+ return null;
333
+ }
334
+ if (launchCommand === undefined) {
335
+ return null;
336
+ }
337
+ if (telemetry === undefined) {
338
+ return null;
339
+ }
340
+ if (controller === undefined) {
341
+ return null;
342
+ }
343
+ return {
344
+ sessionId,
345
+ directoryId,
346
+ tenantId,
347
+ userId,
348
+ workspaceId,
349
+ worktreeId,
350
+ status,
351
+ attentionReason,
352
+ statusModel,
353
+ latestCursor,
354
+ processId,
355
+ attachedClients,
356
+ eventSubscribers,
357
+ startedAt,
358
+ lastEventAt,
359
+ lastExit,
360
+ exitedAt,
361
+ live,
362
+ launchCommand,
363
+ telemetry: telemetry ?? null,
364
+ controller: controller ?? null,
365
+ };
366
+ }
367
+
368
+ export function parseSessionSummaryList(value: unknown): readonly StreamSessionSummary[] {
369
+ if (!Array.isArray(value)) {
370
+ return [];
371
+ }
372
+ const parsed: StreamSessionSummary[] = [];
373
+ for (const entry of value) {
374
+ const summary = parseSessionSummaryRecord(entry);
375
+ if (summary !== null) {
376
+ parsed.push(summary);
377
+ }
378
+ }
379
+ return parsed;
380
+ }
@@ -0,0 +1,21 @@
1
+ import type {
2
+ StreamSessionRuntimeStatus,
3
+ StreamSessionStatusModel,
4
+ StreamTelemetrySummary,
5
+ } from '../stream-protocol.ts';
6
+
7
+ export interface AgentStatusProjectionInput {
8
+ readonly runtimeStatus: StreamSessionRuntimeStatus;
9
+ readonly attentionReason: string | null;
10
+ readonly telemetry: StreamTelemetrySummary | null;
11
+ readonly observedAt: string;
12
+ readonly previous: StreamSessionStatusModel | null;
13
+ }
14
+
15
+ export interface AgentStatusReducer {
16
+ readonly agentType: string;
17
+ project(input: AgentStatusProjectionInput): StreamSessionStatusModel | null;
18
+ }
19
+
20
+ // Runtime token so coverage/deadcode tooling can account for this module.
21
+ export const AGENT_STATUS_REDUCER_RUNTIME_TOKEN = 'agent-status-reducer';
@@ -0,0 +1,170 @@
1
+ import type {
2
+ StreamSessionDisplayPhase,
3
+ StreamSessionRuntimeStatus,
4
+ StreamSessionStatusModel,
5
+ StreamTelemetrySummary,
6
+ } from '../stream-protocol.ts';
7
+ import type { AgentStatusProjectionInput, AgentStatusReducer } from './agent-status-reducer.ts';
8
+
9
+ interface WorkProjection {
10
+ readonly text: string | null;
11
+ readonly phaseHint: 'needs-action' | 'working' | 'idle' | null;
12
+ }
13
+
14
+ function normalizeText(value: string | null): string | null {
15
+ if (value === null) {
16
+ return null;
17
+ }
18
+ const normalized = value.replace(/\s+/gu, ' ').trim();
19
+ return normalized.length > 0 ? normalized : null;
20
+ }
21
+
22
+ function parseIsoMs(value: string | null): number {
23
+ if (value === null) {
24
+ return Number.NaN;
25
+ }
26
+ return Date.parse(value);
27
+ }
28
+
29
+ function eventIsNewer(observedAt: string, previousObservedAt: string | null): boolean {
30
+ const observedAtMs = parseIsoMs(observedAt);
31
+ const previousAtMs = parseIsoMs(previousObservedAt);
32
+ if (!Number.isFinite(previousAtMs) || !Number.isFinite(observedAtMs)) {
33
+ return true;
34
+ }
35
+ return observedAtMs >= previousAtMs;
36
+ }
37
+
38
+ function phaseFromRuntimeStatus(
39
+ runtimeStatus: StreamSessionRuntimeStatus,
40
+ phaseHint: WorkProjection['phaseHint'],
41
+ ): StreamSessionDisplayPhase {
42
+ if (runtimeStatus === 'needs-input') {
43
+ return 'needs-action';
44
+ }
45
+ if (runtimeStatus === 'exited') {
46
+ return 'exited';
47
+ }
48
+ if (phaseHint === 'working') {
49
+ return 'working';
50
+ }
51
+ if (phaseHint === 'needs-action') {
52
+ return 'needs-action';
53
+ }
54
+ if (phaseHint === 'idle') {
55
+ return 'idle';
56
+ }
57
+ if (runtimeStatus === 'running') {
58
+ return 'starting';
59
+ }
60
+ return 'idle';
61
+ }
62
+
63
+ function defaultTextForPhase(phase: StreamSessionDisplayPhase): string {
64
+ if (phase === 'needs-action') {
65
+ return 'needs input';
66
+ }
67
+ if (phase === 'starting') {
68
+ return 'starting';
69
+ }
70
+ if (phase === 'working') {
71
+ return 'active';
72
+ }
73
+ if (phase === 'exited') {
74
+ return 'exited';
75
+ }
76
+ return 'inactive';
77
+ }
78
+
79
+ function glyphForPhase(phase: StreamSessionDisplayPhase): StreamSessionStatusModel['glyph'] {
80
+ if (phase === 'needs-action') {
81
+ return '▲';
82
+ }
83
+ if (phase === 'starting') {
84
+ return '◔';
85
+ }
86
+ if (phase === 'working') {
87
+ return '◆';
88
+ }
89
+ if (phase === 'exited') {
90
+ return '■';
91
+ }
92
+ return '○';
93
+ }
94
+
95
+ function badgeForRuntimeStatus(
96
+ runtimeStatus: StreamSessionRuntimeStatus,
97
+ ): StreamSessionStatusModel['badge'] {
98
+ if (runtimeStatus === 'needs-input') {
99
+ return 'NEED';
100
+ }
101
+ if (runtimeStatus === 'running') {
102
+ return 'RUN ';
103
+ }
104
+ if (runtimeStatus === 'completed') {
105
+ return 'DONE';
106
+ }
107
+ return 'EXIT';
108
+ }
109
+
110
+ export abstract class BaseAgentStatusReducer implements AgentStatusReducer {
111
+ abstract readonly agentType: string;
112
+
113
+ protected constructor() {}
114
+
115
+ project(input: AgentStatusProjectionInput): StreamSessionStatusModel | null {
116
+ const previous = input.previous;
117
+ let workText = previous?.lastKnownWork ?? null;
118
+ let workPhaseHint = previous?.phaseHint ?? null;
119
+ let workObservedAt = previous?.lastKnownWorkAt ?? null;
120
+
121
+ if (input.telemetry !== null && eventIsNewer(input.telemetry.observedAt, workObservedAt)) {
122
+ const projected = this.projectFromTelemetry(input.telemetry);
123
+ if (projected !== null) {
124
+ workText = projected.text;
125
+ workPhaseHint = projected.phaseHint;
126
+ workObservedAt = input.telemetry.observedAt;
127
+ }
128
+ }
129
+
130
+ if (
131
+ input.runtimeStatus === 'completed' &&
132
+ eventIsNewer(input.observedAt, workObservedAt) &&
133
+ workPhaseHint !== 'needs-action'
134
+ ) {
135
+ workText = 'inactive';
136
+ workPhaseHint = 'idle';
137
+ workObservedAt = input.observedAt;
138
+ }
139
+ if (input.runtimeStatus === 'exited' && eventIsNewer(input.observedAt, workObservedAt)) {
140
+ workText = 'exited';
141
+ workPhaseHint = 'idle';
142
+ workObservedAt = input.observedAt;
143
+ }
144
+
145
+ const phase = phaseFromRuntimeStatus(input.runtimeStatus, workPhaseHint);
146
+ const normalizedAttentionReason = normalizeText(input.attentionReason);
147
+ const detailText =
148
+ (input.runtimeStatus === 'needs-input' ? normalizedAttentionReason : null) ??
149
+ workText ??
150
+ normalizedAttentionReason ??
151
+ defaultTextForPhase(phase);
152
+
153
+ return {
154
+ runtimeStatus: input.runtimeStatus,
155
+ phase,
156
+ glyph: glyphForPhase(phase),
157
+ badge: badgeForRuntimeStatus(input.runtimeStatus),
158
+ detailText,
159
+ attentionReason: normalizedAttentionReason,
160
+ lastKnownWork: workText,
161
+ lastKnownWorkAt: workObservedAt,
162
+ phaseHint: workPhaseHint,
163
+ observedAt: input.observedAt,
164
+ };
165
+ }
166
+
167
+ protected projectFromTelemetry(_telemetry: StreamTelemetrySummary): WorkProjection | null {
168
+ return null;
169
+ }
170
+ }
@@ -0,0 +1,37 @@
1
+ import type { StreamTelemetrySummary } from '../../stream-protocol.ts';
2
+ import { BaseAgentStatusReducer } from '../reducer-base.ts';
3
+
4
+ function normalize(value: string | null): string {
5
+ return (value ?? '').trim().toLowerCase();
6
+ }
7
+
8
+ export class ClaudeStatusReducer extends BaseAgentStatusReducer {
9
+ readonly agentType = 'claude';
10
+
11
+ constructor() {
12
+ super();
13
+ }
14
+
15
+ protected override projectFromTelemetry(
16
+ telemetry: StreamTelemetrySummary,
17
+ ): { text: string | null; phaseHint: 'needs-action' | 'working' | 'idle' | null } | null {
18
+ const eventName = normalize(telemetry.eventName);
19
+ if (eventName === 'claude.userpromptsubmit' || eventName === 'claude.pretooluse') {
20
+ return {
21
+ text: 'active',
22
+ phaseHint: 'working',
23
+ };
24
+ }
25
+ if (
26
+ eventName === 'claude.stop' ||
27
+ eventName === 'claude.subagentstop' ||
28
+ eventName === 'claude.sessionend'
29
+ ) {
30
+ return {
31
+ text: 'inactive',
32
+ phaseHint: 'idle',
33
+ };
34
+ }
35
+ return null;
36
+ }
37
+ }
@@ -0,0 +1,48 @@
1
+ import type { StreamTelemetrySummary } from '../../stream-protocol.ts';
2
+ import { BaseAgentStatusReducer } from '../reducer-base.ts';
3
+
4
+ function normalize(value: string | null): string {
5
+ return (value ?? '').trim().toLowerCase();
6
+ }
7
+
8
+ export class CodexStatusReducer extends BaseAgentStatusReducer {
9
+ readonly agentType = 'codex';
10
+
11
+ constructor() {
12
+ super();
13
+ }
14
+
15
+ protected override projectFromTelemetry(
16
+ telemetry: StreamTelemetrySummary,
17
+ ): { text: string | null; phaseHint: 'needs-action' | 'working' | 'idle' | null } | null {
18
+ const eventName = normalize(telemetry.eventName);
19
+ const summary = normalize(telemetry.summary);
20
+ if (eventName === 'codex.user_prompt') {
21
+ return {
22
+ text: 'active',
23
+ phaseHint: 'working',
24
+ };
25
+ }
26
+ if (eventName === 'codex.turn.e2e_duration_ms') {
27
+ return {
28
+ text: 'inactive',
29
+ phaseHint: 'idle',
30
+ };
31
+ }
32
+ if (eventName === 'codex.sse_event') {
33
+ if (
34
+ summary.includes('response.created') ||
35
+ summary.includes('response.in_progress') ||
36
+ summary.includes('response.output_text.delta') ||
37
+ summary.includes('response.output_item.added') ||
38
+ summary.includes('response.function_call_arguments.delta')
39
+ ) {
40
+ return {
41
+ text: 'active',
42
+ phaseHint: 'working',
43
+ };
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ }
@@ -0,0 +1,15 @@
1
+ import type { StreamSessionStatusModel } from '../../stream-protocol.ts';
2
+ import type { AgentStatusProjectionInput } from '../agent-status-reducer.ts';
3
+ import { BaseAgentStatusReducer } from '../reducer-base.ts';
4
+
5
+ export class CritiqueStatusReducer extends BaseAgentStatusReducer {
6
+ readonly agentType = 'critique';
7
+
8
+ constructor() {
9
+ super();
10
+ }
11
+
12
+ override project(_input: AgentStatusProjectionInput): StreamSessionStatusModel | null {
13
+ return null;
14
+ }
15
+ }
@@ -0,0 +1,37 @@
1
+ import type { StreamTelemetrySummary } from '../../stream-protocol.ts';
2
+ import { BaseAgentStatusReducer } from '../reducer-base.ts';
3
+
4
+ function normalize(value: string | null): string {
5
+ return (value ?? '').trim().toLowerCase();
6
+ }
7
+
8
+ export class CursorStatusReducer extends BaseAgentStatusReducer {
9
+ readonly agentType = 'cursor';
10
+
11
+ constructor() {
12
+ super();
13
+ }
14
+
15
+ protected override projectFromTelemetry(
16
+ telemetry: StreamTelemetrySummary,
17
+ ): { text: string | null; phaseHint: 'needs-action' | 'working' | 'idle' | null } | null {
18
+ const eventName = normalize(telemetry.eventName);
19
+ if (
20
+ eventName === 'cursor.beforesubmitprompt' ||
21
+ eventName === 'cursor.beforeshellexecution' ||
22
+ eventName === 'cursor.beforemcptool'
23
+ ) {
24
+ return {
25
+ text: 'active',
26
+ phaseHint: 'working',
27
+ };
28
+ }
29
+ if (eventName === 'cursor.stop' || eventName === 'cursor.sessionend') {
30
+ return {
31
+ text: 'inactive',
32
+ phaseHint: 'idle',
33
+ };
34
+ }
35
+ return null;
36
+ }
37
+ }