@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,132 @@
1
+ import {
2
+ diffTerminalFrames,
3
+ replayTerminalSteps,
4
+ type TerminalSnapshotFrame,
5
+ } from './snapshot-oracle.ts';
6
+
7
+ export type TerminalDifferentialStep =
8
+ | {
9
+ kind: 'output';
10
+ chunk: string;
11
+ }
12
+ | {
13
+ kind: 'resize';
14
+ cols: number;
15
+ rows: number;
16
+ };
17
+
18
+ export interface TerminalDifferentialCheckpoint {
19
+ readonly id: string;
20
+ readonly stepIndex: number;
21
+ readonly directFrameHash: string;
22
+ readonly directFrame?: TerminalSnapshotFrame;
23
+ }
24
+
25
+ export interface TerminalDifferentialCase {
26
+ readonly id: string;
27
+ readonly cols: number;
28
+ readonly rows: number;
29
+ readonly steps: readonly TerminalDifferentialStep[];
30
+ readonly checkpoints: readonly TerminalDifferentialCheckpoint[];
31
+ }
32
+
33
+ export interface TerminalDifferentialCheckpointResult {
34
+ readonly id: string;
35
+ readonly pass: boolean;
36
+ readonly harnessFrameHash: string | null;
37
+ readonly directFrameHash: string;
38
+ readonly reasons: readonly string[];
39
+ }
40
+
41
+ export interface TerminalDifferentialCaseResult {
42
+ readonly id: string;
43
+ readonly pass: boolean;
44
+ readonly checkpointResults: readonly TerminalDifferentialCheckpointResult[];
45
+ }
46
+
47
+ export interface TerminalDifferentialSuiteResult {
48
+ readonly pass: boolean;
49
+ readonly totalCases: number;
50
+ readonly passedCases: number;
51
+ readonly failedCases: number;
52
+ readonly totalCheckpoints: number;
53
+ readonly failedCheckpoints: number;
54
+ readonly caseResults: readonly TerminalDifferentialCaseResult[];
55
+ }
56
+
57
+ function evaluateCheckpoint(
58
+ checkpoint: TerminalDifferentialCheckpoint,
59
+ harnessFrame: TerminalSnapshotFrame | undefined,
60
+ ): TerminalDifferentialCheckpointResult {
61
+ if (harnessFrame === undefined) {
62
+ return {
63
+ id: checkpoint.id,
64
+ pass: false,
65
+ harnessFrameHash: null,
66
+ directFrameHash: checkpoint.directFrameHash,
67
+ reasons: ['checkpoint-step-missing'],
68
+ };
69
+ }
70
+
71
+ const reasons: string[] = [];
72
+ if (harnessFrame.frameHash !== checkpoint.directFrameHash) {
73
+ reasons.push('frame-hash-mismatch');
74
+ }
75
+
76
+ if (checkpoint.directFrame !== undefined) {
77
+ const frameDiff = diffTerminalFrames(checkpoint.directFrame, harnessFrame);
78
+ if (!frameDiff.equal) {
79
+ for (const reason of frameDiff.reasons) {
80
+ reasons.push(`frame-diff:${reason}`);
81
+ }
82
+ }
83
+ }
84
+
85
+ return {
86
+ id: checkpoint.id,
87
+ pass: reasons.length === 0,
88
+ harnessFrameHash: harnessFrame.frameHash,
89
+ directFrameHash: checkpoint.directFrameHash,
90
+ reasons,
91
+ };
92
+ }
93
+
94
+ export function runTerminalDifferentialCase(
95
+ scenario: TerminalDifferentialCase,
96
+ ): TerminalDifferentialCaseResult {
97
+ const frames = replayTerminalSteps(scenario.steps, scenario.cols, scenario.rows);
98
+ const checkpointResults = scenario.checkpoints.map((checkpoint) =>
99
+ evaluateCheckpoint(checkpoint, frames[checkpoint.stepIndex]),
100
+ );
101
+ return {
102
+ id: scenario.id,
103
+ pass: checkpointResults.every((result) => result.pass),
104
+ checkpointResults,
105
+ };
106
+ }
107
+
108
+ export function runTerminalDifferentialSuite(
109
+ scenarios: readonly TerminalDifferentialCase[],
110
+ ): TerminalDifferentialSuiteResult {
111
+ const caseResults = scenarios.map((scenario) => runTerminalDifferentialCase(scenario));
112
+ const passedCases = caseResults.filter((result) => result.pass).length;
113
+ const totalCheckpoints = caseResults.reduce(
114
+ (count, result) => count + result.checkpointResults.length,
115
+ 0,
116
+ );
117
+ const failedCheckpoints = caseResults.reduce(
118
+ (count, result) =>
119
+ count + result.checkpointResults.filter((checkpoint) => !checkpoint.pass).length,
120
+ 0,
121
+ );
122
+
123
+ return {
124
+ pass: passedCases === caseResults.length,
125
+ totalCases: caseResults.length,
126
+ passedCases,
127
+ failedCases: caseResults.length - passedCases,
128
+ totalCheckpoints,
129
+ failedCheckpoints,
130
+ caseResults,
131
+ };
132
+ }
@@ -0,0 +1,441 @@
1
+ import { TerminalSnapshotOracle, type TerminalSnapshotFrame } from './snapshot-oracle.ts';
2
+
3
+ type ActiveScreen = TerminalSnapshotFrame['activeScreen'];
4
+ type SnapshotLine = TerminalSnapshotFrame['richLines'][number];
5
+ type SnapshotCell = SnapshotLine['cells'][number];
6
+ type SnapshotStyle = SnapshotCell['style'];
7
+ type SnapshotColor = SnapshotStyle['fg'];
8
+
9
+ type TerminalParityProfile = 'codex' | 'vim' | 'core';
10
+
11
+ type TerminalParityStep =
12
+ | {
13
+ kind: 'output';
14
+ chunk: string;
15
+ }
16
+ | {
17
+ kind: 'resize';
18
+ cols: number;
19
+ rows: number;
20
+ };
21
+
22
+ interface TerminalParityColorExpectation {
23
+ kind: SnapshotColor['kind'];
24
+ index?: number;
25
+ r?: number;
26
+ g?: number;
27
+ b?: number;
28
+ }
29
+
30
+ interface TerminalParityStyleExpectation {
31
+ bold?: boolean;
32
+ dim?: boolean;
33
+ italic?: boolean;
34
+ underline?: boolean;
35
+ inverse?: boolean;
36
+ fg?: TerminalParityColorExpectation;
37
+ bg?: TerminalParityColorExpectation;
38
+ }
39
+
40
+ interface TerminalParityLineExpectation {
41
+ row: number;
42
+ equals?: string;
43
+ includes?: string;
44
+ wrapped?: boolean;
45
+ }
46
+
47
+ interface TerminalParityCellExpectation {
48
+ row: number;
49
+ col: number;
50
+ glyph?: string;
51
+ width?: number;
52
+ continued?: boolean;
53
+ style?: TerminalParityStyleExpectation;
54
+ }
55
+
56
+ interface TerminalParityExpectations {
57
+ activeScreen?: ActiveScreen;
58
+ cursor?: {
59
+ row?: number;
60
+ col?: number;
61
+ visible?: boolean;
62
+ };
63
+ viewport?: {
64
+ followOutput?: boolean;
65
+ topMin?: number;
66
+ topMax?: number;
67
+ };
68
+ lines?: readonly TerminalParityLineExpectation[];
69
+ cells?: readonly TerminalParityCellExpectation[];
70
+ }
71
+
72
+ export interface TerminalParityScene {
73
+ id: string;
74
+ profile: TerminalParityProfile;
75
+ description: string;
76
+ cols: number;
77
+ rows: number;
78
+ steps: readonly TerminalParityStep[];
79
+ expectations: TerminalParityExpectations;
80
+ }
81
+
82
+ interface TerminalParitySceneResult {
83
+ sceneId: string;
84
+ profile: TerminalParityProfile;
85
+ pass: boolean;
86
+ failures: string[];
87
+ frameHash: string;
88
+ finalFrame: TerminalSnapshotFrame;
89
+ }
90
+
91
+ interface TerminalParityMatrixResult {
92
+ pass: boolean;
93
+ totalScenes: number;
94
+ passedScenes: number;
95
+ failedScenes: number;
96
+ results: TerminalParitySceneResult[];
97
+ }
98
+
99
+ function colorMatches(actual: SnapshotColor, expected: TerminalParityColorExpectation): boolean {
100
+ if (actual.kind !== expected.kind) {
101
+ return false;
102
+ }
103
+ switch (actual.kind) {
104
+ case 'default':
105
+ return true;
106
+ case 'indexed':
107
+ return actual.index === expected.index;
108
+ case 'rgb':
109
+ return actual.r === expected.r && actual.g === expected.g && actual.b === expected.b;
110
+ }
111
+ }
112
+
113
+ function styleMatches(actual: SnapshotStyle, expected: TerminalParityStyleExpectation): string[] {
114
+ const failures: string[] = [];
115
+ if (expected.bold !== undefined && actual.bold !== expected.bold) {
116
+ failures.push('style.bold');
117
+ }
118
+ if (expected.dim !== undefined && actual.dim !== expected.dim) {
119
+ failures.push('style.dim');
120
+ }
121
+ if (expected.italic !== undefined && actual.italic !== expected.italic) {
122
+ failures.push('style.italic');
123
+ }
124
+ if (expected.underline !== undefined && actual.underline !== expected.underline) {
125
+ failures.push('style.underline');
126
+ }
127
+ if (expected.inverse !== undefined && actual.inverse !== expected.inverse) {
128
+ failures.push('style.inverse');
129
+ }
130
+ if (expected.fg !== undefined && !colorMatches(actual.fg, expected.fg)) {
131
+ failures.push('style.fg');
132
+ }
133
+ if (expected.bg !== undefined && !colorMatches(actual.bg, expected.bg)) {
134
+ failures.push('style.bg');
135
+ }
136
+ return failures;
137
+ }
138
+
139
+ export function runTerminalParityScene(scene: TerminalParityScene): TerminalParitySceneResult {
140
+ const oracle = new TerminalSnapshotOracle(scene.cols, scene.rows);
141
+ for (const step of scene.steps) {
142
+ if (step.kind === 'output') {
143
+ oracle.ingest(step.chunk);
144
+ continue;
145
+ }
146
+ oracle.resize(step.cols, step.rows);
147
+ }
148
+
149
+ const finalFrame = oracle.snapshot();
150
+ const failures: string[] = [];
151
+ const expectations = scene.expectations;
152
+
153
+ if (
154
+ expectations.activeScreen !== undefined &&
155
+ finalFrame.activeScreen !== expectations.activeScreen
156
+ ) {
157
+ failures.push('active-screen');
158
+ }
159
+ if (expectations.cursor?.row !== undefined && finalFrame.cursor.row !== expectations.cursor.row) {
160
+ failures.push('cursor-row');
161
+ }
162
+ if (expectations.cursor?.col !== undefined && finalFrame.cursor.col !== expectations.cursor.col) {
163
+ failures.push('cursor-col');
164
+ }
165
+ if (
166
+ expectations.cursor?.visible !== undefined &&
167
+ finalFrame.cursor.visible !== expectations.cursor.visible
168
+ ) {
169
+ failures.push('cursor-visible');
170
+ }
171
+ if (
172
+ expectations.viewport?.followOutput !== undefined &&
173
+ finalFrame.viewport.followOutput !== expectations.viewport.followOutput
174
+ ) {
175
+ failures.push('viewport-follow-output');
176
+ }
177
+ if (
178
+ expectations.viewport?.topMin !== undefined &&
179
+ finalFrame.viewport.top < expectations.viewport.topMin
180
+ ) {
181
+ failures.push('viewport-top-min');
182
+ }
183
+ if (
184
+ expectations.viewport?.topMax !== undefined &&
185
+ finalFrame.viewport.top > expectations.viewport.topMax
186
+ ) {
187
+ failures.push('viewport-top-max');
188
+ }
189
+
190
+ for (const expectedLine of expectations.lines ?? []) {
191
+ const line = finalFrame.richLines[expectedLine.row];
192
+ if (line === undefined) {
193
+ failures.push(`line-${String(expectedLine.row)}-missing`);
194
+ continue;
195
+ }
196
+ if (expectedLine.equals !== undefined && line.text !== expectedLine.equals) {
197
+ failures.push(`line-${String(expectedLine.row)}-equals`);
198
+ }
199
+ if (expectedLine.includes !== undefined && !line.text.includes(expectedLine.includes)) {
200
+ failures.push(`line-${String(expectedLine.row)}-includes`);
201
+ }
202
+ if (expectedLine.wrapped !== undefined && line.wrapped !== expectedLine.wrapped) {
203
+ failures.push(`line-${String(expectedLine.row)}-wrapped`);
204
+ }
205
+ }
206
+
207
+ for (const expectedCell of expectations.cells ?? []) {
208
+ const line = finalFrame.richLines[expectedCell.row];
209
+ const cell = line?.cells[expectedCell.col];
210
+ if (cell === undefined) {
211
+ failures.push(`cell-${String(expectedCell.row)}-${String(expectedCell.col)}-missing`);
212
+ continue;
213
+ }
214
+ if (expectedCell.glyph !== undefined && cell.glyph !== expectedCell.glyph) {
215
+ failures.push(`cell-${String(expectedCell.row)}-${String(expectedCell.col)}-glyph`);
216
+ }
217
+ if (expectedCell.width !== undefined && cell.width !== expectedCell.width) {
218
+ failures.push(`cell-${String(expectedCell.row)}-${String(expectedCell.col)}-width`);
219
+ }
220
+ if (expectedCell.continued !== undefined && cell.continued !== expectedCell.continued) {
221
+ failures.push(`cell-${String(expectedCell.row)}-${String(expectedCell.col)}-continued`);
222
+ }
223
+ if (expectedCell.style !== undefined) {
224
+ for (const styleFailure of styleMatches(cell.style, expectedCell.style)) {
225
+ failures.push(
226
+ `cell-${String(expectedCell.row)}-${String(expectedCell.col)}-${styleFailure}`,
227
+ );
228
+ }
229
+ }
230
+ }
231
+
232
+ return {
233
+ sceneId: scene.id,
234
+ profile: scene.profile,
235
+ pass: failures.length === 0,
236
+ failures,
237
+ frameHash: finalFrame.frameHash,
238
+ finalFrame,
239
+ };
240
+ }
241
+
242
+ export function runTerminalParityMatrix(
243
+ scenes: readonly TerminalParityScene[],
244
+ ): TerminalParityMatrixResult {
245
+ const results = scenes.map((scene) => runTerminalParityScene(scene));
246
+ const passedScenes = results.filter((result) => result.pass).length;
247
+ return {
248
+ pass: passedScenes === scenes.length,
249
+ totalScenes: scenes.length,
250
+ passedScenes,
251
+ failedScenes: scenes.length - passedScenes,
252
+ results,
253
+ };
254
+ }
255
+
256
+ export const TERMINAL_PARITY_SCENES: readonly TerminalParityScene[] = [
257
+ {
258
+ id: 'codex-pinned-footer-scroll-region',
259
+ profile: 'codex',
260
+ description: 'Footer/status rows remain pinned while output scrolls above.',
261
+ cols: 40,
262
+ rows: 8,
263
+ steps: [
264
+ { kind: 'output', chunk: '\u001b[1;6r' },
265
+ { kind: 'output', chunk: '\u001b[7;1H> chat bar' },
266
+ { kind: 'output', chunk: '\u001b[8;1H? status bar' },
267
+ { kind: 'output', chunk: '\u001b[6;1Hline-1\nline-2\nline-3\nline-4\nline-5\nline-6' },
268
+ ],
269
+ expectations: {
270
+ lines: [
271
+ { row: 6, includes: '> chat bar' },
272
+ { row: 7, includes: '? status bar' },
273
+ ],
274
+ },
275
+ },
276
+ {
277
+ id: 'codex-footer-background-persistence',
278
+ profile: 'codex',
279
+ description: 'Footer prompt row keeps truecolor background while scroll region churns above.',
280
+ cols: 36,
281
+ rows: 8,
282
+ steps: [
283
+ { kind: 'output', chunk: '\u001b[1;6r' },
284
+ { kind: 'output', chunk: '\u001b[7;1H\u001b[48;2;25;30;36m> prompt\u001b[0m' },
285
+ { kind: 'output', chunk: '\u001b[8;1H\u001b[48;5;236mstatus\u001b[0m' },
286
+ { kind: 'output', chunk: '\u001b[6;1Hline-1\nline-2\nline-3\nline-4\nline-5\nline-6' },
287
+ ],
288
+ expectations: {
289
+ lines: [
290
+ { row: 6, includes: '> prompt' },
291
+ { row: 7, includes: 'status' },
292
+ ],
293
+ cells: [
294
+ {
295
+ row: 6,
296
+ col: 0,
297
+ glyph: '>',
298
+ style: {
299
+ bg: {
300
+ kind: 'rgb',
301
+ r: 25,
302
+ g: 30,
303
+ b: 36,
304
+ },
305
+ },
306
+ },
307
+ {
308
+ row: 7,
309
+ col: 0,
310
+ glyph: 's',
311
+ style: {
312
+ bg: {
313
+ kind: 'indexed',
314
+ index: 236,
315
+ },
316
+ },
317
+ },
318
+ ],
319
+ },
320
+ },
321
+ {
322
+ id: 'codex-origin-and-background',
323
+ profile: 'codex',
324
+ description:
325
+ 'Origin mode addresses relative to scroll region and true background color is retained.',
326
+ cols: 20,
327
+ rows: 6,
328
+ steps: [
329
+ { kind: 'output', chunk: '\u001b[2;5r' },
330
+ { kind: 'output', chunk: '\u001b[?6h\u001b[1;1H' },
331
+ { kind: 'output', chunk: '\u001b[48;2;10;20;30mX' },
332
+ { kind: 'output', chunk: '\u001b[?6l\u001b[1;1HY' },
333
+ ],
334
+ expectations: {
335
+ cells: [
336
+ {
337
+ row: 1,
338
+ col: 0,
339
+ glyph: 'X',
340
+ style: {
341
+ bg: {
342
+ kind: 'rgb',
343
+ r: 10,
344
+ g: 20,
345
+ b: 30,
346
+ },
347
+ },
348
+ },
349
+ {
350
+ row: 0,
351
+ col: 0,
352
+ glyph: 'Y',
353
+ },
354
+ ],
355
+ },
356
+ },
357
+ {
358
+ id: 'vim-scroll-reverse-index',
359
+ profile: 'vim',
360
+ description: 'Reverse index at top margin scrolls region down, preserving bottom status row.',
361
+ cols: 24,
362
+ rows: 6,
363
+ steps: [
364
+ { kind: 'output', chunk: '\u001b[1;5r' },
365
+ { kind: 'output', chunk: 'a\nb\nc\nd\n' },
366
+ { kind: 'output', chunk: '\u001b[6;1Hstatus' },
367
+ { kind: 'output', chunk: '\u001b[1;1H\u001bMtop' },
368
+ ],
369
+ expectations: {
370
+ lines: [
371
+ { row: 0, includes: 'top' },
372
+ { row: 5, includes: 'status' },
373
+ ],
374
+ },
375
+ },
376
+ {
377
+ id: 'core-pending-wrap',
378
+ profile: 'core',
379
+ description: 'Glyph at right margin wraps only when next printable glyph arrives.',
380
+ cols: 5,
381
+ rows: 3,
382
+ steps: [
383
+ { kind: 'output', chunk: 'abcde' },
384
+ { kind: 'output', chunk: '\u001b[31m' },
385
+ { kind: 'output', chunk: 'f' },
386
+ ],
387
+ expectations: {
388
+ lines: [
389
+ { row: 0, equals: 'abcde' },
390
+ { row: 1, equals: 'f' },
391
+ ],
392
+ cells: [
393
+ {
394
+ row: 1,
395
+ col: 0,
396
+ style: {
397
+ fg: { kind: 'indexed', index: 1 },
398
+ },
399
+ },
400
+ ],
401
+ },
402
+ },
403
+ {
404
+ id: 'core-wrap-tab-insert-delete-char',
405
+ profile: 'core',
406
+ description: 'Pending wrap, default tab stops, and insert/delete character semantics.',
407
+ cols: 16,
408
+ rows: 4,
409
+ steps: [
410
+ { kind: 'output', chunk: 'abcde' },
411
+ { kind: 'output', chunk: '\u001b[31m' },
412
+ { kind: 'output', chunk: 'Z' },
413
+ { kind: 'output', chunk: '\r\tX' },
414
+ { kind: 'output', chunk: '\u001b[2;1Habcdef' },
415
+ { kind: 'output', chunk: '\u001b[2;3H\u001b[2@\u001b[1P' },
416
+ ],
417
+ expectations: {
418
+ lines: [
419
+ { row: 0, equals: 'abcdeZ X' },
420
+ { row: 1, includes: 'ab cdef' },
421
+ ],
422
+ cells: [
423
+ {
424
+ row: 0,
425
+ col: 8,
426
+ glyph: 'X',
427
+ },
428
+ {
429
+ row: 1,
430
+ col: 0,
431
+ glyph: 'a',
432
+ },
433
+ {
434
+ row: 1,
435
+ col: 2,
436
+ glyph: ' ',
437
+ },
438
+ ],
439
+ },
440
+ },
441
+ ];