@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,78 @@
1
+ interface ConversationDoubleClickState {
2
+ readonly conversationId: string;
3
+ readonly atMs: number;
4
+ }
5
+
6
+ interface ConversationDoubleClickResult {
7
+ readonly doubleClick: boolean;
8
+ readonly nextState: ConversationDoubleClickState | null;
9
+ }
10
+
11
+ interface EntityDoubleClickState {
12
+ readonly entityId: string;
13
+ readonly atMs: number;
14
+ }
15
+
16
+ interface EntityDoubleClickResult {
17
+ readonly doubleClick: boolean;
18
+ readonly nextState: EntityDoubleClickState | null;
19
+ }
20
+
21
+ export function detectEntityDoubleClick(
22
+ previous: EntityDoubleClickState | null,
23
+ entityId: string,
24
+ nowMs: number,
25
+ windowMs: number,
26
+ ): EntityDoubleClickResult {
27
+ const elapsedMs = previous === null ? Number.POSITIVE_INFINITY : nowMs - previous.atMs;
28
+ if (
29
+ previous !== null &&
30
+ previous.entityId === entityId &&
31
+ elapsedMs >= 0 &&
32
+ elapsedMs <= windowMs
33
+ ) {
34
+ return {
35
+ doubleClick: true,
36
+ nextState: null,
37
+ };
38
+ }
39
+ return {
40
+ doubleClick: false,
41
+ nextState: {
42
+ entityId,
43
+ atMs: nowMs,
44
+ },
45
+ };
46
+ }
47
+
48
+ export function detectConversationDoubleClick(
49
+ previous: ConversationDoubleClickState | null,
50
+ conversationId: string,
51
+ nowMs: number,
52
+ windowMs: number,
53
+ ): ConversationDoubleClickResult {
54
+ const generic = detectEntityDoubleClick(
55
+ previous === null
56
+ ? null
57
+ : {
58
+ entityId: previous.conversationId,
59
+ atMs: previous.atMs,
60
+ },
61
+ conversationId,
62
+ nowMs,
63
+ windowMs,
64
+ );
65
+ if (generic.nextState === null) {
66
+ return {
67
+ doubleClick: generic.doubleClick,
68
+ nextState: null,
69
+ };
70
+ }
71
+ return {
72
+ doubleClick: generic.doubleClick,
73
+ nextState: {
74
+ conversationId: generic.nextState.entityId,
75
+ atMs: generic.nextState.atMs,
76
+ },
77
+ };
78
+ }
@@ -0,0 +1,435 @@
1
+ import { measureDisplayWidth, wrapTextForColumns } from '../terminal/snapshot-oracle.ts';
2
+
3
+ const MIN_LEFT_PANE_COLS = 28;
4
+ const MIN_RIGHT_PANE_COLS = 20;
5
+ const LEFT_RATIO_NUMERATOR = 30;
6
+ const LEFT_RATIO_DENOMINATOR = 100;
7
+ const SCROLL_STEP_ROWS = 1;
8
+
9
+ interface DualPaneLayout {
10
+ readonly cols: number;
11
+ readonly rows: number;
12
+ readonly paneRows: number;
13
+ readonly statusRow: number;
14
+ readonly leftCols: number;
15
+ readonly rightCols: number;
16
+ readonly separatorCol: number;
17
+ readonly rightStartCol: number;
18
+ }
19
+
20
+ type PaneTarget = 'left' | 'right' | 'separator' | 'status' | 'outside';
21
+
22
+ interface SgrMouseEvent {
23
+ readonly sequence: string;
24
+ readonly code: number;
25
+ readonly col: number;
26
+ readonly row: number;
27
+ readonly final: 'M' | 'm';
28
+ }
29
+
30
+ type MuxInputToken =
31
+ | {
32
+ readonly kind: 'passthrough';
33
+ readonly text: string;
34
+ }
35
+ | {
36
+ readonly kind: 'mouse';
37
+ readonly event: SgrMouseEvent;
38
+ };
39
+
40
+ interface ParsedMuxInput {
41
+ readonly tokens: readonly MuxInputToken[];
42
+ readonly remainder: string;
43
+ }
44
+
45
+ interface RoutedMuxInput {
46
+ readonly forwardToSession: readonly Buffer[];
47
+ readonly leftPaneScrollRows: number;
48
+ readonly rightPaneScrollRows: number;
49
+ }
50
+
51
+ interface EventPaneView {
52
+ readonly lines: readonly string[];
53
+ readonly followOutput: boolean;
54
+ readonly top: number;
55
+ readonly totalRows: number;
56
+ }
57
+
58
+ interface ComputeDualPaneLayoutOptions {
59
+ readonly leftCols?: number | null;
60
+ }
61
+
62
+ function clamp(value: number, min: number, max: number): number {
63
+ if (value < min) {
64
+ return min;
65
+ }
66
+ if (value > max) {
67
+ return max;
68
+ }
69
+ return value;
70
+ }
71
+
72
+ function resolveLeftPaneCols(normalizedCols: number, requestedLeftCols: number | null): number {
73
+ const availablePaneCols = normalizedCols - 1;
74
+ const defaultLeftCols = Math.floor(
75
+ (normalizedCols * LEFT_RATIO_NUMERATOR) / LEFT_RATIO_DENOMINATOR,
76
+ );
77
+ const requested = requestedLeftCols === null ? defaultLeftCols : Math.floor(requestedLeftCols);
78
+
79
+ let leftCols = clamp(requested, 1, availablePaneCols - 1);
80
+ if (normalizedCols >= MIN_LEFT_PANE_COLS + MIN_RIGHT_PANE_COLS + 1) {
81
+ leftCols = Math.max(MIN_LEFT_PANE_COLS, leftCols);
82
+ const maxLeft = availablePaneCols - MIN_RIGHT_PANE_COLS;
83
+ leftCols = Math.min(leftCols, maxLeft);
84
+ }
85
+ return leftCols;
86
+ }
87
+
88
+ export function computeDualPaneLayout(
89
+ cols: number,
90
+ rows: number,
91
+ options: ComputeDualPaneLayoutOptions = {},
92
+ ): DualPaneLayout {
93
+ const normalizedCols = Math.max(3, cols);
94
+ const normalizedRows = Math.max(2, rows);
95
+ const paneRows = Math.max(1, normalizedRows - 1);
96
+ const statusRow = paneRows + 1;
97
+
98
+ const availablePaneCols = normalizedCols - 1;
99
+ const leftCols = resolveLeftPaneCols(normalizedCols, options.leftCols ?? null);
100
+ const rightCols = availablePaneCols - leftCols;
101
+
102
+ return {
103
+ cols: normalizedCols,
104
+ rows: normalizedRows,
105
+ paneRows,
106
+ statusRow,
107
+ leftCols,
108
+ rightCols,
109
+ separatorCol: leftCols + 1,
110
+ rightStartCol: leftCols + 2,
111
+ };
112
+ }
113
+
114
+ export function classifyPaneAt(layout: DualPaneLayout, col: number, row: number): PaneTarget {
115
+ if (col < 1 || row < 1 || col > layout.cols || row > layout.rows) {
116
+ return 'outside';
117
+ }
118
+
119
+ if (row === layout.statusRow) {
120
+ return 'status';
121
+ }
122
+
123
+ if (col <= layout.leftCols) {
124
+ return 'left';
125
+ }
126
+
127
+ if (col === layout.separatorCol) {
128
+ return 'separator';
129
+ }
130
+
131
+ return 'right';
132
+ }
133
+
134
+ const SGR_MOUSE_PREFIX = '\u001b[<';
135
+ const NUMERIC_SGR_BODY = /^\d+;\d+;\d+$/;
136
+ const PARTIAL_SGR_BODY = /^[0-9;]*$/;
137
+
138
+ function parseSgrMouseEvent(sequence: string): SgrMouseEvent | null {
139
+ const final = sequence.endsWith('m') ? 'm' : 'M';
140
+
141
+ const body = sequence.slice(SGR_MOUSE_PREFIX.length, -1);
142
+ if (!NUMERIC_SGR_BODY.test(body)) {
143
+ return null;
144
+ }
145
+
146
+ const [codePart, colPart, rowPart] = body.split(';');
147
+ const code = Number.parseInt(codePart!, 10);
148
+ const col = Number.parseInt(colPart!, 10);
149
+ const row = Number.parseInt(rowPart!, 10);
150
+
151
+ return {
152
+ sequence,
153
+ code,
154
+ col,
155
+ row,
156
+ final,
157
+ };
158
+ }
159
+
160
+ function splitPartialMouseTail(text: string): { passthrough: string; remainder: string } {
161
+ const tailStart = text.lastIndexOf(SGR_MOUSE_PREFIX);
162
+ if (tailStart < 0) {
163
+ return {
164
+ passthrough: text,
165
+ remainder: '',
166
+ };
167
+ }
168
+
169
+ const candidate = text.slice(tailStart + SGR_MOUSE_PREFIX.length);
170
+ if (PARTIAL_SGR_BODY.test(candidate)) {
171
+ return {
172
+ passthrough: text.slice(0, tailStart),
173
+ remainder: text.slice(tailStart),
174
+ };
175
+ }
176
+
177
+ return {
178
+ passthrough: text,
179
+ remainder: '',
180
+ };
181
+ }
182
+
183
+ export function parseMuxInputChunk(previousRemainder: string, chunk: Buffer): ParsedMuxInput {
184
+ const input = `${previousRemainder}${chunk.toString('utf8')}`;
185
+ const tokens: MuxInputToken[] = [];
186
+
187
+ let cursor = 0;
188
+ while (cursor < input.length) {
189
+ const start = input.indexOf(SGR_MOUSE_PREFIX, cursor);
190
+ if (start < 0) {
191
+ break;
192
+ }
193
+
194
+ if (start > cursor) {
195
+ tokens.push({
196
+ kind: 'passthrough',
197
+ text: input.slice(cursor, start),
198
+ });
199
+ }
200
+
201
+ let end = -1;
202
+ let index = start + SGR_MOUSE_PREFIX.length;
203
+ while (index < input.length) {
204
+ const char = input[index]!;
205
+ if (char === 'M' || char === 'm') {
206
+ end = index;
207
+ break;
208
+ }
209
+ const isDigit = char >= '0' && char <= '9';
210
+ if (isDigit || char === ';') {
211
+ index += 1;
212
+ continue;
213
+ }
214
+ break;
215
+ }
216
+
217
+ if (end < 0) {
218
+ cursor = start;
219
+ break;
220
+ }
221
+
222
+ const sequence = input.slice(start, end + 1);
223
+ const parsed = parseSgrMouseEvent(sequence);
224
+ if (parsed === null) {
225
+ tokens.push({
226
+ kind: 'passthrough',
227
+ text: sequence,
228
+ });
229
+ } else {
230
+ tokens.push({
231
+ kind: 'mouse',
232
+ event: parsed,
233
+ });
234
+ }
235
+
236
+ cursor = end + 1;
237
+ }
238
+
239
+ const tail = input.slice(cursor);
240
+ const splitTail = splitPartialMouseTail(tail);
241
+ if (splitTail.passthrough.length > 0) {
242
+ tokens.push({
243
+ kind: 'passthrough',
244
+ text: splitTail.passthrough,
245
+ });
246
+ }
247
+
248
+ return {
249
+ tokens,
250
+ remainder: splitTail.remainder,
251
+ };
252
+ }
253
+
254
+ export function wheelDeltaRowsFromCode(code: number): number | null {
255
+ if ((code & 0b0100_0000) === 0) {
256
+ return null;
257
+ }
258
+
259
+ return (code & 0b0000_0001) === 0 ? -SCROLL_STEP_ROWS : SCROLL_STEP_ROWS;
260
+ }
261
+
262
+ export function routeMuxInputTokens(
263
+ tokens: readonly MuxInputToken[],
264
+ layout: DualPaneLayout,
265
+ ): RoutedMuxInput {
266
+ const forwardToSession: Buffer[] = [];
267
+ let leftPaneScrollRows = 0;
268
+ let rightPaneScrollRows = 0;
269
+
270
+ for (const token of tokens) {
271
+ if (token.kind === 'passthrough') {
272
+ if (token.text.length > 0) {
273
+ forwardToSession.push(Buffer.from(token.text, 'utf8'));
274
+ }
275
+ continue;
276
+ }
277
+
278
+ const target = classifyPaneAt(layout, token.event.col, token.event.row);
279
+ if (target === 'right') {
280
+ const deltaRows = wheelDeltaRowsFromCode(token.event.code);
281
+ if (deltaRows !== null) {
282
+ rightPaneScrollRows += deltaRows;
283
+ }
284
+ continue;
285
+ }
286
+
287
+ if (target === 'left') {
288
+ const deltaRows = wheelDeltaRowsFromCode(token.event.code);
289
+ if (deltaRows !== null) {
290
+ leftPaneScrollRows += deltaRows;
291
+ continue;
292
+ }
293
+ forwardToSession.push(Buffer.from(token.event.sequence, 'utf8'));
294
+ }
295
+ }
296
+
297
+ return {
298
+ forwardToSession,
299
+ leftPaneScrollRows,
300
+ rightPaneScrollRows,
301
+ };
302
+ }
303
+
304
+ function wrappedEventRows(lines: readonly string[], cols: number): string[] {
305
+ if (lines.length === 0) {
306
+ return [''];
307
+ }
308
+
309
+ const wrapped: string[] = [];
310
+ for (const line of lines) {
311
+ wrapped.push(...wrapTextForColumns(line, cols));
312
+ }
313
+ return wrapped;
314
+ }
315
+
316
+ export class EventPaneViewport {
317
+ private readonly maxLines: number;
318
+
319
+ private readonly lines: string[] = [];
320
+
321
+ private followOutput = true;
322
+
323
+ private top = 0;
324
+
325
+ constructor(maxLines = 1000) {
326
+ this.maxLines = Math.max(1, maxLines);
327
+ }
328
+
329
+ append(line: string): void {
330
+ this.lines.push(line);
331
+ while (this.lines.length > this.maxLines) {
332
+ this.lines.shift();
333
+ }
334
+ }
335
+
336
+ view(cols: number, paneRows: number): EventPaneView {
337
+ const safeCols = Math.max(1, cols);
338
+ const safeRows = Math.max(1, paneRows);
339
+ const wrapped = wrappedEventRows(this.lines, safeCols);
340
+ const maxTop = Math.max(0, wrapped.length - safeRows);
341
+
342
+ if (this.followOutput) {
343
+ this.top = maxTop;
344
+ } else {
345
+ this.top = clamp(this.top, 0, maxTop);
346
+ if (this.top === maxTop) {
347
+ this.followOutput = true;
348
+ }
349
+ }
350
+
351
+ const rendered = wrapped.slice(this.top, this.top + safeRows);
352
+ while (rendered.length < safeRows) {
353
+ rendered.push('');
354
+ }
355
+
356
+ return {
357
+ lines: rendered,
358
+ followOutput: this.followOutput,
359
+ top: this.top,
360
+ totalRows: wrapped.length,
361
+ };
362
+ }
363
+
364
+ scrollBy(deltaRows: number, cols: number, paneRows: number): EventPaneView {
365
+ const safeCols = Math.max(1, cols);
366
+ const safeRows = Math.max(1, paneRows);
367
+ const wrapped = wrappedEventRows(this.lines, safeCols);
368
+ const maxTop = Math.max(0, wrapped.length - safeRows);
369
+
370
+ const baselineTop = this.followOutput ? maxTop : this.top;
371
+ this.top = clamp(baselineTop + deltaRows, 0, maxTop);
372
+ this.followOutput = this.top === maxTop;
373
+
374
+ return this.view(safeCols, safeRows);
375
+ }
376
+ }
377
+
378
+ export function padOrTrimDisplay(text: string, width: number): string {
379
+ if (width <= 0) {
380
+ return '';
381
+ }
382
+
383
+ let output = '';
384
+ let outputWidth = 0;
385
+ for (const char of text) {
386
+ const charWidth = Math.max(1, measureDisplayWidth(char));
387
+ if (outputWidth + charWidth > width) {
388
+ break;
389
+ }
390
+
391
+ output += char;
392
+ outputWidth += charWidth;
393
+ }
394
+
395
+ if (outputWidth < width) {
396
+ output += ' '.repeat(width - outputWidth);
397
+ }
398
+
399
+ return output;
400
+ }
401
+
402
+ interface DiffRenderedRowsResult {
403
+ readonly output: string;
404
+ readonly nextRows: readonly string[];
405
+ readonly changedRows: readonly number[];
406
+ }
407
+
408
+ export function diffRenderedRows(
409
+ currentRows: readonly string[],
410
+ previousRows: readonly string[],
411
+ ): DiffRenderedRowsResult {
412
+ const changedRows: number[] = [];
413
+ let output = '';
414
+
415
+ const rowCount = Math.max(currentRows.length, previousRows.length);
416
+ const nextRows: string[] = [];
417
+ for (let row = 0; row < rowCount; row += 1) {
418
+ const current = currentRows[row] ?? '';
419
+ const previous = previousRows[row] ?? '';
420
+ nextRows.push(current);
421
+
422
+ if (current === previous) {
423
+ continue;
424
+ }
425
+
426
+ changedRows.push(row);
427
+ output += `\u001b[${String(row + 1)};1H\u001b[2K${current}`;
428
+ }
429
+
430
+ return {
431
+ output,
432
+ nextRows,
433
+ changedRows,
434
+ };
435
+ }