@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,247 @@
1
+ import {
2
+ createUiSurface,
3
+ DEFAULT_UI_STYLE,
4
+ drawUiText,
5
+ fillUiRow,
6
+ renderUiSurfaceAnsiRows,
7
+ type UiStyle,
8
+ } from '../ui/surface.ts';
9
+ import { paintUiRow } from '../ui/kit.ts';
10
+ import { measureDisplayWidth } from '../terminal/snapshot-oracle.ts';
11
+ import { buildWorkspaceRailViewRows } from './workspace-rail-model.ts';
12
+ import { getActiveMuxTheme, type MuxWorkspaceRailTheme } from '../ui/mux-theme.ts';
13
+
14
+ type WorkspaceRailModel = Parameters<typeof buildWorkspaceRailViewRows>[0];
15
+ type WorkspaceRailViewRow = ReturnType<typeof buildWorkspaceRailViewRows>[number];
16
+
17
+ const INLINE_THREAD_BUTTON_LABEL = '[+ thread]';
18
+
19
+ function conversationStatusIconStyle(
20
+ status: WorkspaceRailViewRow['conversationStatus'],
21
+ active: boolean,
22
+ theme: MuxWorkspaceRailTheme,
23
+ ): UiStyle {
24
+ return {
25
+ fg:
26
+ status === 'working'
27
+ ? theme.statusColors.working
28
+ : status === 'exited'
29
+ ? theme.statusColors.exited
30
+ : status === 'needs-action'
31
+ ? theme.statusColors.needsAction
32
+ : status === 'starting'
33
+ ? theme.statusColors.starting
34
+ : theme.statusColors.idle,
35
+ bg: active ? theme.activeRowStyle.bg : { kind: 'default' },
36
+ bold: status === 'working',
37
+ };
38
+ }
39
+
40
+ function treeTextStartColumn(text: string): number {
41
+ let index = 0;
42
+ while (index < text.length) {
43
+ const token = text[index]!;
44
+ if (token === '│' || token === '├' || token === '└' || token === '─' || token === ' ') {
45
+ index += 1;
46
+ continue;
47
+ }
48
+ break;
49
+ }
50
+ return index;
51
+ }
52
+
53
+ function drawTreeRow(
54
+ surface: ReturnType<typeof createUiSurface>,
55
+ rowIndex: number,
56
+ row: WorkspaceRailViewRow,
57
+ theme: MuxWorkspaceRailTheme,
58
+ contentStyle: UiStyle,
59
+ activeContentStyle: UiStyle | null,
60
+ options: {
61
+ buttonLabel?: string;
62
+ buttonStyle?: UiStyle;
63
+ alignButtonRight?: boolean;
64
+ } = {},
65
+ ): void {
66
+ fillUiRow(surface, rowIndex, theme.normalStyle);
67
+ const buttonLabel = options.buttonLabel;
68
+ const buttonStyle = options.buttonStyle;
69
+ const alignButtonRight = options.alignButtonRight ?? false;
70
+ const buttonTextStart = buttonLabel === undefined ? -1 : row.text.lastIndexOf(buttonLabel);
71
+ const baseRowText =
72
+ alignButtonRight && buttonLabel !== undefined && buttonTextStart >= 0
73
+ ? row.text.slice(0, buttonTextStart).trimEnd()
74
+ : row.text;
75
+ const contentStart = treeTextStartColumn(baseRowText);
76
+ const treePrefix = baseRowText.slice(0, contentStart);
77
+ const content = baseRowText.slice(contentStart);
78
+ drawUiText(surface, 0, rowIndex, treePrefix, theme.mutedStyle);
79
+ drawUiText(
80
+ surface,
81
+ contentStart,
82
+ rowIndex,
83
+ content,
84
+ row.active && activeContentStyle !== null ? activeContentStyle : contentStyle,
85
+ );
86
+ if (buttonLabel === undefined || buttonStyle === undefined) {
87
+ return;
88
+ }
89
+ if (buttonTextStart < 0) {
90
+ return;
91
+ }
92
+ const buttonWidth = Math.max(1, measureDisplayWidth(buttonLabel));
93
+ const buttonStart = alignButtonRight ? Math.max(0, surface.cols - buttonWidth) : buttonTextStart;
94
+ drawUiText(surface, buttonStart, rowIndex, buttonLabel, buttonStyle);
95
+ }
96
+
97
+ function drawActionRow(
98
+ surface: ReturnType<typeof createUiSurface>,
99
+ rowIndex: number,
100
+ row: WorkspaceRailViewRow,
101
+ theme: MuxWorkspaceRailTheme,
102
+ ): void {
103
+ fillUiRow(surface, rowIndex, theme.normalStyle);
104
+ if (row.railAction === 'project.add') {
105
+ drawUiText(surface, 0, rowIndex, '│', theme.mutedStyle);
106
+ const buttonStart = Math.max(0, Math.floor((surface.cols - row.text.length) / 2));
107
+ drawUiText(surface, buttonStart, rowIndex, row.text, theme.actionStyle);
108
+ return;
109
+ }
110
+ drawUiText(surface, 0, rowIndex, row.text, theme.mutedStyle);
111
+ const buttonStart = row.text.indexOf('[');
112
+ const buttonEnd = row.text.lastIndexOf(']');
113
+ const safeButtonStart = Math.max(0, buttonStart);
114
+ drawUiText(
115
+ surface,
116
+ safeButtonStart,
117
+ rowIndex,
118
+ row.text.slice(safeButtonStart, Math.max(safeButtonStart, buttonEnd + 1)),
119
+ theme.actionStyle,
120
+ );
121
+ }
122
+
123
+ function drawDirectoryHeaderRow(
124
+ surface: ReturnType<typeof createUiSurface>,
125
+ rowIndex: number,
126
+ row: WorkspaceRailViewRow,
127
+ theme: MuxWorkspaceRailTheme,
128
+ ): void {
129
+ drawTreeRow(surface, rowIndex, row, theme, theme.headerStyle, theme.activeRowStyle, {
130
+ buttonLabel: INLINE_THREAD_BUTTON_LABEL,
131
+ buttonStyle: theme.actionStyle,
132
+ alignButtonRight: true,
133
+ });
134
+ }
135
+
136
+ function drawConversationRow(
137
+ surface: ReturnType<typeof createUiSurface>,
138
+ rowIndex: number,
139
+ row: WorkspaceRailViewRow,
140
+ theme: MuxWorkspaceRailTheme,
141
+ ): void {
142
+ const rowStyle =
143
+ row.kind === 'conversation-body' ? theme.conversationBodyStyle : theme.normalStyle;
144
+ drawTreeRow(surface, rowIndex, row, theme, rowStyle, theme.activeRowStyle);
145
+ if (row.kind !== 'conversation-title') {
146
+ return;
147
+ }
148
+ const statusStyle = conversationStatusIconStyle(row.conversationStatus, row.active, theme);
149
+ const statusMatch = row.text.match(/[▲◔◆○■]/u);
150
+ if (statusMatch === null || statusMatch.index === undefined) {
151
+ return;
152
+ }
153
+ drawUiText(surface, statusMatch.index, rowIndex, statusMatch[0], statusStyle);
154
+ }
155
+
156
+ function paintWorkspaceRailRow(
157
+ surface: ReturnType<typeof createUiSurface>,
158
+ rowIndex: number,
159
+ row: WorkspaceRailViewRow,
160
+ theme: MuxWorkspaceRailTheme,
161
+ ): void {
162
+ if (row.kind === 'dir-header') {
163
+ drawDirectoryHeaderRow(surface, rowIndex, row, theme);
164
+ return;
165
+ }
166
+ if (row.kind === 'dir-meta') {
167
+ drawTreeRow(surface, rowIndex, row, theme, theme.metaStyle, theme.activeRowStyle);
168
+ return;
169
+ }
170
+ if (row.kind === 'conversation-title' || row.kind === 'conversation-body') {
171
+ drawConversationRow(surface, rowIndex, row, theme);
172
+ return;
173
+ }
174
+ if (row.kind === 'process-title' || row.kind === 'process-meta') {
175
+ paintUiRow(surface, rowIndex, row.text, theme.processStyle, theme.normalStyle);
176
+ return;
177
+ }
178
+ if (row.kind === 'repository-header') {
179
+ const buttonLabel = row.text.endsWith('[+]') ? '[+]' : row.text.endsWith('[-]') ? '[-]' : null;
180
+ if (buttonLabel === null) {
181
+ drawTreeRow(surface, rowIndex, row, theme, theme.headerStyle, theme.activeRowStyle);
182
+ } else {
183
+ drawTreeRow(surface, rowIndex, row, theme, theme.headerStyle, theme.activeRowStyle, {
184
+ buttonLabel,
185
+ buttonStyle: theme.actionStyle,
186
+ });
187
+ }
188
+ return;
189
+ }
190
+ if (row.kind === 'repository-row') {
191
+ paintUiRow(surface, rowIndex, row.text, theme.repositoryRowStyle, theme.normalStyle);
192
+ return;
193
+ }
194
+ if (row.kind === 'shortcut-header') {
195
+ const buttonLabel = row.text.endsWith('[+]') ? '[+]' : row.text.endsWith('[-]') ? '[-]' : null;
196
+ if (buttonLabel === null) {
197
+ drawTreeRow(surface, rowIndex, row, theme, theme.headerStyle, theme.activeRowStyle);
198
+ } else {
199
+ drawTreeRow(surface, rowIndex, row, theme, theme.headerStyle, theme.activeRowStyle, {
200
+ buttonLabel,
201
+ buttonStyle: theme.actionStyle,
202
+ });
203
+ }
204
+ return;
205
+ }
206
+ if (row.kind === 'shortcut-body') {
207
+ paintUiRow(surface, rowIndex, row.text, theme.shortcutStyle);
208
+ return;
209
+ }
210
+ if (row.kind === 'action') {
211
+ drawActionRow(surface, rowIndex, row, theme);
212
+ return;
213
+ }
214
+ if (row.kind === 'muted') {
215
+ paintUiRow(surface, rowIndex, row.text, theme.mutedStyle, theme.normalStyle);
216
+ }
217
+ }
218
+
219
+ export function renderWorkspaceRailRowAnsiForTest(
220
+ row: WorkspaceRailViewRow,
221
+ width: number,
222
+ ): string {
223
+ const safeWidth = Math.max(1, width);
224
+ const theme = getActiveMuxTheme().workspaceRail;
225
+ const surface = createUiSurface(safeWidth, 1, DEFAULT_UI_STYLE);
226
+ paintWorkspaceRailRow(surface, 0, row, theme);
227
+ return renderUiSurfaceAnsiRows(surface)[0]!;
228
+ }
229
+
230
+ export function renderWorkspaceRailAnsiRows(
231
+ model: WorkspaceRailModel,
232
+ width: number,
233
+ maxRows: number,
234
+ ): readonly string[] {
235
+ const safeWidth = Math.max(1, width);
236
+ const safeRows = Math.max(1, maxRows);
237
+ const theme = getActiveMuxTheme().workspaceRail;
238
+ const rows = buildWorkspaceRailViewRows(model, safeRows);
239
+ const surface = createUiSurface(safeWidth, safeRows, DEFAULT_UI_STYLE);
240
+
241
+ for (let rowIndex = 0; rowIndex < safeRows; rowIndex += 1) {
242
+ const row = rows[rowIndex]!;
243
+ paintWorkspaceRailRow(surface, rowIndex, row, theme);
244
+ }
245
+
246
+ return renderUiSurfaceAnsiRows(surface);
247
+ }
@@ -0,0 +1,307 @@
1
+ import { closeSync, mkdirSync, openSync, writeSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+
4
+ type PerfAttrValue = boolean | number | string;
5
+ type PerfAttrs = Readonly<Record<string, PerfAttrValue>>;
6
+
7
+ interface PerfCoreConfig {
8
+ enabled: boolean;
9
+ filePath?: string;
10
+ }
11
+
12
+ interface PerfEventRecord {
13
+ type: 'event';
14
+ name: string;
15
+ 'ts-ns': string;
16
+ 'ts-ms': number;
17
+ attrs?: PerfAttrs;
18
+ }
19
+
20
+ interface PerfSpanRecord {
21
+ type: 'span';
22
+ name: string;
23
+ 'start-ns': string;
24
+ 'duration-ns': string;
25
+ 'end-ms': number;
26
+ 'trace-id': string;
27
+ 'span-id': string;
28
+ 'parent-span-id'?: string;
29
+ attrs?: PerfAttrs;
30
+ }
31
+
32
+ type PerfRecord = PerfEventRecord | PerfSpanRecord;
33
+
34
+ const DEFAULT_FILE_PATH = '.harness/perf.jsonl';
35
+ const DEFAULT_MAX_PENDING_RECORDS = 4096;
36
+ const DEFAULT_EVENT_SAMPLE_RATES: Readonly<Record<string, number>> = {
37
+ 'pty.stdout.chunk': 0.1,
38
+ };
39
+
40
+ const state: {
41
+ enabled: boolean;
42
+ filePath: string;
43
+ fd: number | null;
44
+ nextTraceId: number;
45
+ nextSpanId: number;
46
+ flushTimer: NodeJS.Timeout | null;
47
+ pendingRecords: string[];
48
+ maxPendingRecords: number;
49
+ sampleRates: Readonly<Record<string, number>>;
50
+ sampleCounters: Map<string, number>;
51
+ } = {
52
+ enabled: false,
53
+ filePath: DEFAULT_FILE_PATH,
54
+ fd: null,
55
+ nextTraceId: 1,
56
+ nextSpanId: 1,
57
+ flushTimer: null,
58
+ pendingRecords: [],
59
+ maxPendingRecords: DEFAULT_MAX_PENDING_RECORDS,
60
+ sampleRates: DEFAULT_EVENT_SAMPLE_RATES,
61
+ sampleCounters: new Map(),
62
+ };
63
+
64
+ function ensureWriter(): void {
65
+ if (!state.enabled || state.fd !== null) {
66
+ return;
67
+ }
68
+
69
+ const resolvedPath = resolve(state.filePath);
70
+ mkdirSync(dirname(resolvedPath), { recursive: true });
71
+ state.fd = openSync(resolvedPath, 'a');
72
+ }
73
+
74
+ function closeWriter(): void {
75
+ flushPendingRecords();
76
+ if (state.flushTimer !== null) {
77
+ clearTimeout(state.flushTimer);
78
+ state.flushTimer = null;
79
+ }
80
+ if (state.fd === null) {
81
+ return;
82
+ }
83
+
84
+ closeSync(state.fd);
85
+ state.fd = null;
86
+ }
87
+
88
+ function scheduleFlush(): void {
89
+ if (!state.enabled || state.fd === null || state.flushTimer !== null) {
90
+ return;
91
+ }
92
+ state.flushTimer = setTimeout(() => {
93
+ state.flushTimer = null;
94
+ flushPendingRecords();
95
+ }, 0);
96
+ state.flushTimer.unref();
97
+ }
98
+
99
+ function flushPendingRecords(): void {
100
+ if (state.fd === null || state.pendingRecords.length === 0) {
101
+ return;
102
+ }
103
+ const chunk = state.pendingRecords.join('');
104
+ state.pendingRecords.length = 0;
105
+ writeSync(state.fd, chunk);
106
+ }
107
+
108
+ function writeRecord(record: PerfRecord): void {
109
+ ensureWriter();
110
+ if (state.fd === null) {
111
+ return;
112
+ }
113
+ if (state.pendingRecords.length >= state.maxPendingRecords) {
114
+ state.pendingRecords.shift();
115
+ }
116
+ state.pendingRecords.push(`${JSON.stringify(record)}\n`);
117
+ scheduleFlush();
118
+ }
119
+
120
+ function shouldRecordEvent(name: string): boolean {
121
+ const sampleRate = state.sampleRates[name];
122
+ if (sampleRate === undefined) {
123
+ return true;
124
+ }
125
+ if (!Number.isFinite(sampleRate) || sampleRate <= 0) {
126
+ return false;
127
+ }
128
+ if (sampleRate >= 1) {
129
+ return true;
130
+ }
131
+ const sampleEvery = Math.max(1, Math.floor(1 / sampleRate));
132
+ const previous = state.sampleCounters.get(name) ?? 0;
133
+ const next = previous + 1;
134
+ state.sampleCounters.set(name, next);
135
+ return next % sampleEvery === 0;
136
+ }
137
+
138
+ function nextTraceId(): string {
139
+ const traceId = `trace-${state.nextTraceId}`;
140
+ state.nextTraceId += 1;
141
+ return traceId;
142
+ }
143
+
144
+ function nextSpanId(): string {
145
+ const spanId = `span-${state.nextSpanId}`;
146
+ state.nextSpanId += 1;
147
+ return spanId;
148
+ }
149
+
150
+ function mergeAttrs(base?: PerfAttrs, extra?: PerfAttrs): PerfAttrs | undefined {
151
+ if (base === undefined && extra === undefined) {
152
+ return undefined;
153
+ }
154
+
155
+ if (base === undefined) {
156
+ return extra;
157
+ }
158
+
159
+ if (extra === undefined) {
160
+ return base;
161
+ }
162
+
163
+ return { ...base, ...extra };
164
+ }
165
+
166
+ function writeDurationRecord(
167
+ name: string,
168
+ startedAtNs: bigint,
169
+ attrs?: PerfAttrs,
170
+ traceId?: string,
171
+ spanId?: string,
172
+ parentSpanId?: string,
173
+ ): void {
174
+ if (!state.enabled) {
175
+ return;
176
+ }
177
+
178
+ const endedAtNs = perfNowNs();
179
+ const endedAtMs = Date.now();
180
+ const record: PerfSpanRecord = {
181
+ type: 'span',
182
+ name,
183
+ 'start-ns': startedAtNs.toString(),
184
+ 'duration-ns': (endedAtNs - startedAtNs).toString(),
185
+ 'end-ms': endedAtMs,
186
+ 'trace-id': traceId ?? nextTraceId(),
187
+ 'span-id': spanId ?? nextSpanId(),
188
+ };
189
+
190
+ if (parentSpanId !== undefined) {
191
+ record['parent-span-id'] = parentSpanId;
192
+ }
193
+ if (attrs !== undefined) {
194
+ record.attrs = attrs;
195
+ }
196
+
197
+ writeRecord(record);
198
+ }
199
+
200
+ class ActivePerfSpan {
201
+ private ended = false;
202
+ private readonly name: string;
203
+ private readonly startedAtNs: bigint;
204
+ private readonly attrs: PerfAttrs | undefined;
205
+ private readonly traceId: string;
206
+ private readonly spanId: string;
207
+ private readonly parentSpanId: string | undefined;
208
+
209
+ constructor(
210
+ name: string,
211
+ startedAtNs: bigint,
212
+ attrs: PerfAttrs | undefined,
213
+ traceId: string,
214
+ spanId: string,
215
+ parentSpanId: string | undefined,
216
+ ) {
217
+ this.name = name;
218
+ this.startedAtNs = startedAtNs;
219
+ this.attrs = attrs;
220
+ this.traceId = traceId;
221
+ this.spanId = spanId;
222
+ this.parentSpanId = parentSpanId;
223
+ }
224
+
225
+ end(extraAttrs?: PerfAttrs): void {
226
+ if (this.ended) {
227
+ return;
228
+ }
229
+ this.ended = true;
230
+ writeDurationRecord(
231
+ this.name,
232
+ this.startedAtNs,
233
+ mergeAttrs(this.attrs, extraAttrs),
234
+ this.traceId,
235
+ this.spanId,
236
+ this.parentSpanId,
237
+ );
238
+ }
239
+ }
240
+
241
+ interface PerfSpan {
242
+ end(extraAttrs?: PerfAttrs): void;
243
+ }
244
+
245
+ const NOOP_PERF_SPAN: PerfSpan = {
246
+ end(): void {
247
+ return;
248
+ },
249
+ };
250
+
251
+ export function configurePerfCore(config: PerfCoreConfig): void {
252
+ const nextFilePath = config.filePath ?? state.filePath;
253
+ const pathChanged = resolve(nextFilePath) !== resolve(state.filePath);
254
+
255
+ if (pathChanged || (!config.enabled && state.enabled)) {
256
+ closeWriter();
257
+ }
258
+
259
+ state.enabled = config.enabled;
260
+ state.filePath = nextFilePath;
261
+ state.sampleCounters.clear();
262
+
263
+ if (state.enabled) {
264
+ ensureWriter();
265
+ }
266
+ }
267
+
268
+ export function isPerfCoreEnabled(): boolean {
269
+ return state.enabled;
270
+ }
271
+
272
+ export function perfNowNs(): bigint {
273
+ return process.hrtime.bigint();
274
+ }
275
+
276
+ export function startPerfSpan(name: string, attrs?: PerfAttrs, parentSpanId?: string): PerfSpan {
277
+ if (!state.enabled) {
278
+ return NOOP_PERF_SPAN;
279
+ }
280
+
281
+ return new ActivePerfSpan(name, perfNowNs(), attrs, nextTraceId(), nextSpanId(), parentSpanId);
282
+ }
283
+
284
+ export function recordPerfDuration(name: string, startedAtNs: bigint, attrs?: PerfAttrs): void {
285
+ writeDurationRecord(name, startedAtNs, attrs);
286
+ }
287
+
288
+ export function recordPerfEvent(name: string, attrs?: PerfAttrs): void {
289
+ if (!state.enabled || !shouldRecordEvent(name)) {
290
+ return;
291
+ }
292
+
293
+ const record: PerfEventRecord = {
294
+ type: 'event',
295
+ name,
296
+ 'ts-ns': perfNowNs().toString(),
297
+ 'ts-ms': Date.now(),
298
+ };
299
+ if (attrs !== undefined) {
300
+ record.attrs = attrs;
301
+ }
302
+ writeRecord(record);
303
+ }
304
+
305
+ export function shutdownPerfCore(): void {
306
+ closeWriter();
307
+ }