@jmoyers/harness 0.1.11 → 0.1.20

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 (232) hide show
  1. package/README.md +31 -39
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/packages/harness-ui/src/modal-manager.ts +222 -0
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3872
  38. package/scripts/control-plane-daemon.ts +11 -0
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3019
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/gateway/runtime.ts +1872 -0
  46. package/src/cli/parsing/flags.ts +23 -0
  47. package/src/cli/parsing/session.ts +42 -0
  48. package/src/cli/runtime/context.ts +193 -0
  49. package/src/cli/runtime-app/application.ts +392 -0
  50. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  51. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  52. package/src/cli/workflows/runtime.ts +965 -0
  53. package/src/clients/tui/left-rail-interactions.ts +519 -0
  54. package/src/clients/tui/main-pane-interactions.ts +509 -0
  55. package/src/clients/tui/modal-input-routing.ts +71 -0
  56. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  57. package/src/clients/web/synced-selectors.ts +132 -0
  58. package/src/codex/live-session.ts +82 -29
  59. package/src/config/config-core.ts +348 -8
  60. package/src/config/harness.config.template.jsonc +33 -0
  61. package/src/control-plane/agent-realtime-api.ts +82 -427
  62. package/src/control-plane/session-summary.ts +10 -81
  63. package/src/control-plane/status/reducer-base.ts +12 -12
  64. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  65. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  66. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  67. package/src/control-plane/stream-client.ts +12 -2
  68. package/src/control-plane/stream-command-parser.ts +83 -143
  69. package/src/control-plane/stream-protocol.ts +53 -37
  70. package/src/control-plane/stream-server-command.ts +376 -69
  71. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  72. package/src/control-plane/stream-server.ts +864 -70
  73. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  74. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  75. package/src/core/state/observed-stream-cursor.ts +43 -0
  76. package/src/core/state/synced-observed-state.ts +273 -0
  77. package/src/core/store/harness-synced-store.ts +81 -0
  78. package/src/diff/budget.ts +136 -0
  79. package/src/diff/build.ts +289 -0
  80. package/src/diff/chunker.ts +146 -0
  81. package/src/diff/git-invoke.ts +315 -0
  82. package/src/diff/git-parse.ts +472 -0
  83. package/src/diff/hash.ts +70 -0
  84. package/src/diff/index.ts +24 -0
  85. package/src/diff/normalize.ts +134 -0
  86. package/src/diff/types.ts +178 -0
  87. package/src/diff-ui/args.ts +346 -0
  88. package/src/diff-ui/commands.ts +123 -0
  89. package/src/diff-ui/finder.ts +94 -0
  90. package/src/diff-ui/highlight.ts +127 -0
  91. package/src/diff-ui/index.ts +2 -0
  92. package/src/diff-ui/model.ts +141 -0
  93. package/src/diff-ui/pager.ts +412 -0
  94. package/src/diff-ui/render.ts +337 -0
  95. package/src/diff-ui/runtime.ts +379 -0
  96. package/src/diff-ui/state.ts +224 -0
  97. package/src/diff-ui/types.ts +236 -0
  98. package/src/domain/workspace.ts +68 -5
  99. package/src/mux/control-plane-op-queue.ts +93 -7
  100. package/src/mux/conversation-rail.ts +28 -71
  101. package/src/mux/dual-pane-core.ts +13 -13
  102. package/src/mux/harness-core-ui.ts +313 -42
  103. package/src/mux/input-shortcuts.ts +13 -131
  104. package/src/mux/keybinding-catalog.ts +340 -0
  105. package/src/mux/keybinding-registry.ts +103 -0
  106. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  107. package/src/mux/live-mux/command-menu.ts +167 -4
  108. package/src/mux/live-mux/conversation-state.ts +13 -0
  109. package/src/mux/live-mux/directory-resolution.ts +1 -1
  110. package/src/mux/live-mux/git-snapshot.ts +33 -2
  111. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  112. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  113. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  114. package/src/mux/live-mux/input-forwarding.ts +59 -2
  115. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  116. package/src/mux/live-mux/left-nav.ts +35 -0
  117. package/src/mux/live-mux/link-click.ts +292 -0
  118. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  119. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  120. package/src/mux/live-mux/modal-input-reducers.ts +77 -12
  121. package/src/mux/live-mux/modal-overlays.ts +168 -34
  122. package/src/mux/live-mux/modal-pointer.ts +3 -7
  123. package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
  124. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  125. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  126. package/src/mux/live-mux/pointer-routing.ts +5 -2
  127. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  128. package/src/mux/live-mux/rail-layout.ts +33 -30
  129. package/src/mux/live-mux/release-notes.ts +383 -0
  130. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  131. package/src/mux/live-mux/repository-folding.ts +3 -0
  132. package/src/mux/live-mux/selection.ts +0 -4
  133. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  134. package/src/mux/project-pane-github-review.ts +271 -0
  135. package/src/mux/render-frame.ts +4 -0
  136. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  137. package/src/mux/task-composer.ts +21 -14
  138. package/src/mux/task-focused-pane.ts +118 -117
  139. package/src/mux/task-screen-keybindings.ts +10 -101
  140. package/src/mux/workspace-rail-model.ts +270 -104
  141. package/src/mux/workspace-rail.ts +45 -22
  142. package/src/pty/session-broker.ts +1 -1
  143. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  144. package/src/services/control-plane.ts +50 -32
  145. package/src/services/conversation-lifecycle.ts +118 -87
  146. package/src/services/conversation-startup-hydration.ts +20 -12
  147. package/src/services/directory-hydration.ts +21 -16
  148. package/src/services/event-persistence.ts +7 -0
  149. package/src/services/left-rail-pointer-handler.ts +329 -0
  150. package/src/services/mux-ui-state-persistence.ts +5 -1
  151. package/src/services/recording.ts +34 -26
  152. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  153. package/src/services/runtime-control-actions.ts +79 -61
  154. package/src/services/runtime-control-plane-ops.ts +122 -83
  155. package/src/services/runtime-conversation-actions.ts +40 -26
  156. package/src/services/runtime-conversation-activation.ts +73 -46
  157. package/src/services/runtime-conversation-starter.ts +53 -45
  158. package/src/services/runtime-conversation-title-edit.ts +91 -80
  159. package/src/services/runtime-envelope-handler.ts +107 -105
  160. package/src/services/runtime-git-state.ts +42 -29
  161. package/src/services/runtime-layout-resize.ts +3 -1
  162. package/src/services/runtime-left-rail-render.ts +99 -63
  163. package/src/services/runtime-nim-cli-session.ts +438 -0
  164. package/src/services/runtime-nim-session.ts +705 -0
  165. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  166. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  167. package/src/services/runtime-process-wiring.ts +29 -36
  168. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  169. package/src/services/runtime-render-flush.ts +63 -70
  170. package/src/services/runtime-render-lifecycle.ts +65 -64
  171. package/src/services/runtime-render-orchestrator.ts +55 -45
  172. package/src/services/runtime-render-pipeline.ts +106 -103
  173. package/src/services/runtime-render-state.ts +62 -49
  174. package/src/services/runtime-repository-actions.ts +97 -72
  175. package/src/services/runtime-right-pane-render.ts +80 -53
  176. package/src/services/runtime-shutdown.ts +38 -35
  177. package/src/services/runtime-stream-subscriptions.ts +35 -27
  178. package/src/services/runtime-task-composer-persistence.ts +71 -59
  179. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  180. package/src/services/runtime-task-editor-actions.ts +46 -29
  181. package/src/services/runtime-task-pane-actions.ts +220 -134
  182. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  183. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  184. package/src/services/runtime-workspace-observed-events.ts +33 -184
  185. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  186. package/src/services/session-diagnostics-store.ts +217 -0
  187. package/src/services/startup-background-resume.ts +26 -21
  188. package/src/services/startup-orchestrator.ts +16 -13
  189. package/src/services/startup-paint-tracker.ts +29 -21
  190. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  191. package/src/services/startup-settled-gate.ts +25 -15
  192. package/src/services/startup-shutdown.ts +18 -22
  193. package/src/services/startup-state-hydration.ts +44 -34
  194. package/src/services/startup-visibility.ts +12 -4
  195. package/src/services/task-pane-selection-actions.ts +89 -72
  196. package/src/services/task-planning-hydration.ts +24 -18
  197. package/src/services/task-planning-observed-events.ts +50 -52
  198. package/src/services/workspace-observed-events.ts +66 -63
  199. package/src/storage/storage-lifecycle-core.ts +438 -0
  200. package/src/store/control-plane-store-normalize.ts +33 -242
  201. package/src/store/control-plane-store-types.ts +1 -35
  202. package/src/store/control-plane-store.ts +360 -56
  203. package/src/store/event-store.ts +366 -8
  204. package/src/terminal/snapshot-oracle.ts +207 -94
  205. package/src/ui/mux-theme.ts +112 -8
  206. package/src/ui/panes/home-gridfire.ts +40 -31
  207. package/src/ui/panes/home.ts +10 -2
  208. package/src/ui/panes/nim.ts +315 -0
  209. package/src/mux/live-mux/actions-task.ts +0 -115
  210. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  211. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -85
  212. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  213. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  214. package/src/services/runtime-directory-actions.ts +0 -164
  215. package/src/services/runtime-input-pipeline.ts +0 -50
  216. package/src/services/runtime-input-router.ts +0 -195
  217. package/src/services/runtime-main-pane-input.ts +0 -230
  218. package/src/services/runtime-modal-input.ts +0 -137
  219. package/src/services/runtime-navigation-input.ts +0 -197
  220. package/src/services/runtime-rail-input.ts +0 -279
  221. package/src/services/runtime-task-pane.ts +0 -62
  222. package/src/services/runtime-workspace-actions.ts +0 -158
  223. package/src/ui/conversation-input-forwarder.ts +0 -114
  224. package/src/ui/conversation-selection-input.ts +0 -103
  225. package/src/ui/global-shortcut-input.ts +0 -89
  226. package/src/ui/input.ts +0 -269
  227. package/src/ui/kit.ts +0 -509
  228. package/src/ui/left-nav-input.ts +0 -80
  229. package/src/ui/left-rail-pointer-input.ts +0 -148
  230. package/src/ui/modals/manager.ts +0 -218
  231. package/src/ui/repository-fold-input.ts +0 -91
  232. package/src/ui/surface.ts +0 -224
@@ -0,0 +1,238 @@
1
+ const MIN_LEFT_PANE_COLS = 28;
2
+ const MIN_RIGHT_PANE_COLS = 20;
3
+ const DEFAULT_LEFT_PANE_WIDTH_PERCENT = 30;
4
+ const MIN_PANE_WIDTH_PERCENT = 1;
5
+ const MAX_PANE_WIDTH_PERCENT = 99;
6
+ const DEFAULT_BASE_LAYER_Z_INDEX = 0;
7
+ const DEFAULT_OVERLAY_LAYER_Z_INDEX = 100;
8
+
9
+ export interface UiLayoutRect {
10
+ readonly col: number;
11
+ readonly row: number;
12
+ readonly cols: number;
13
+ readonly rows: number;
14
+ }
15
+
16
+ export type UiLayoutAnchor = 'viewport' | 'left-pane' | 'right-pane' | 'status';
17
+
18
+ export interface UiLayoutOverlay {
19
+ readonly id: string;
20
+ readonly col: number;
21
+ readonly row: number;
22
+ readonly cols: number;
23
+ readonly rows: number;
24
+ readonly anchor?: UiLayoutAnchor;
25
+ readonly zIndex?: number;
26
+ readonly clipToViewport?: boolean;
27
+ }
28
+
29
+ export interface ComputeUiLayoutOptions {
30
+ readonly leftCols?: number;
31
+ readonly paneWidthPercent?: number;
32
+ readonly statusRows?: number;
33
+ readonly overlays?: readonly UiLayoutOverlay[];
34
+ }
35
+
36
+ export interface UiLayoutLayer {
37
+ readonly id: string;
38
+ readonly kind: 'left-pane' | 'separator' | 'right-pane' | 'status' | 'overlay';
39
+ readonly zIndex: number;
40
+ readonly rect: UiLayoutRect;
41
+ }
42
+
43
+ export interface UiDualPaneLayout {
44
+ readonly cols: number;
45
+ readonly rows: number;
46
+ readonly paneRows: number;
47
+ readonly statusRow: number;
48
+ readonly leftCols: number;
49
+ readonly rightCols: number;
50
+ readonly separatorCol: number;
51
+ readonly rightStartCol: number;
52
+ readonly viewport: UiLayoutRect;
53
+ readonly leftPane: UiLayoutRect;
54
+ readonly separator: UiLayoutRect;
55
+ readonly rightPane: UiLayoutRect;
56
+ readonly status: UiLayoutRect;
57
+ readonly layers: readonly UiLayoutLayer[];
58
+ }
59
+
60
+ function clamp(value: number, min: number, max: number): number {
61
+ if (value < min) {
62
+ return min;
63
+ }
64
+ if (value > max) {
65
+ return max;
66
+ }
67
+ return value;
68
+ }
69
+
70
+ function normalizeInt(value: number, minimum: number): number {
71
+ if (!Number.isFinite(value)) {
72
+ return minimum;
73
+ }
74
+ return Math.max(minimum, Math.floor(value));
75
+ }
76
+
77
+ function normalizePaneWidthPercent(value: number): number {
78
+ if (!Number.isFinite(value)) {
79
+ return DEFAULT_LEFT_PANE_WIDTH_PERCENT;
80
+ }
81
+ return clamp(value, MIN_PANE_WIDTH_PERCENT, MAX_PANE_WIDTH_PERCENT);
82
+ }
83
+
84
+ function resolveLeftPaneCols(
85
+ normalizedCols: number,
86
+ requestedLeftCols: number | undefined,
87
+ paneWidthPercent: number | undefined,
88
+ ): number {
89
+ const availablePaneCols = normalizedCols - 1;
90
+ const percent = normalizePaneWidthPercent(paneWidthPercent ?? DEFAULT_LEFT_PANE_WIDTH_PERCENT);
91
+ const defaultLeftCols = Math.round((availablePaneCols * percent) / 100);
92
+ const requested =
93
+ requestedLeftCols === undefined ? defaultLeftCols : Math.floor(requestedLeftCols);
94
+
95
+ let leftCols = clamp(requested, 1, availablePaneCols - 1);
96
+ if (normalizedCols >= MIN_LEFT_PANE_COLS + MIN_RIGHT_PANE_COLS + 1) {
97
+ leftCols = Math.max(MIN_LEFT_PANE_COLS, leftCols);
98
+ const maxLeft = availablePaneCols - MIN_RIGHT_PANE_COLS;
99
+ leftCols = Math.min(leftCols, maxLeft);
100
+ }
101
+ return leftCols;
102
+ }
103
+
104
+ function rect(col: number, row: number, cols: number, rows: number): UiLayoutRect {
105
+ return {
106
+ col,
107
+ row,
108
+ cols: Math.max(1, cols),
109
+ rows: Math.max(1, rows),
110
+ };
111
+ }
112
+
113
+ function anchorRect(
114
+ layout: Omit<UiDualPaneLayout, 'layers'>,
115
+ anchor: UiLayoutAnchor,
116
+ ): UiLayoutRect {
117
+ if (anchor === 'left-pane') {
118
+ return layout.leftPane;
119
+ }
120
+ if (anchor === 'right-pane') {
121
+ return layout.rightPane;
122
+ }
123
+ if (anchor === 'status') {
124
+ return layout.status;
125
+ }
126
+ return layout.viewport;
127
+ }
128
+
129
+ function intersectRect(left: UiLayoutRect, right: UiLayoutRect): UiLayoutRect | null {
130
+ const startCol = Math.max(left.col, right.col);
131
+ const startRow = Math.max(left.row, right.row);
132
+ const endCol = Math.min(left.col + left.cols - 1, right.col + right.cols - 1);
133
+ const endRow = Math.min(left.row + left.rows - 1, right.row + right.rows - 1);
134
+ if (endCol < startCol || endRow < startRow) {
135
+ return null;
136
+ }
137
+ return rect(startCol, startRow, endCol - startCol + 1, endRow - startRow + 1);
138
+ }
139
+
140
+ export function computeDualPaneLayoutWithLayers(
141
+ cols: number,
142
+ rows: number,
143
+ options: ComputeUiLayoutOptions = {},
144
+ ): UiDualPaneLayout {
145
+ const normalizedCols = normalizeInt(cols, 3);
146
+ const normalizedRows = normalizeInt(rows, 2);
147
+ const requestedStatusRows = normalizeInt(options.statusRows ?? 1, 1);
148
+ const statusRows = Math.min(requestedStatusRows, normalizedRows - 1);
149
+ const paneRows = normalizedRows - statusRows;
150
+ const statusRow = paneRows + 1;
151
+
152
+ const availablePaneCols = normalizedCols - 1;
153
+ const leftCols = resolveLeftPaneCols(normalizedCols, options.leftCols, options.paneWidthPercent);
154
+ const rightCols = availablePaneCols - leftCols;
155
+ const separatorCol = leftCols + 1;
156
+ const rightStartCol = leftCols + 2;
157
+
158
+ const layoutBase = {
159
+ cols: normalizedCols,
160
+ rows: normalizedRows,
161
+ paneRows,
162
+ statusRow,
163
+ leftCols,
164
+ rightCols,
165
+ separatorCol,
166
+ rightStartCol,
167
+ viewport: rect(1, 1, normalizedCols, normalizedRows),
168
+ leftPane: rect(1, 1, leftCols, paneRows),
169
+ separator: rect(separatorCol, 1, 1, paneRows),
170
+ rightPane: rect(rightStartCol, 1, rightCols, paneRows),
171
+ status: rect(1, statusRow, normalizedCols, statusRows),
172
+ } satisfies Omit<UiDualPaneLayout, 'layers'>;
173
+
174
+ const layers: UiLayoutLayer[] = [
175
+ {
176
+ id: 'left-pane',
177
+ kind: 'left-pane',
178
+ zIndex: DEFAULT_BASE_LAYER_Z_INDEX,
179
+ rect: layoutBase.leftPane,
180
+ },
181
+ {
182
+ id: 'separator',
183
+ kind: 'separator',
184
+ zIndex: DEFAULT_BASE_LAYER_Z_INDEX,
185
+ rect: layoutBase.separator,
186
+ },
187
+ {
188
+ id: 'right-pane',
189
+ kind: 'right-pane',
190
+ zIndex: DEFAULT_BASE_LAYER_Z_INDEX,
191
+ rect: layoutBase.rightPane,
192
+ },
193
+ {
194
+ id: 'status',
195
+ kind: 'status',
196
+ zIndex: DEFAULT_BASE_LAYER_Z_INDEX,
197
+ rect: layoutBase.status,
198
+ },
199
+ ];
200
+
201
+ for (const overlay of options.overlays ?? []) {
202
+ if (overlay.id.trim().length === 0) {
203
+ continue;
204
+ }
205
+ const overlayAnchor = anchorRect(layoutBase, overlay.anchor ?? 'viewport');
206
+ const absoluteRect = rect(
207
+ overlayAnchor.col + overlay.col - 1,
208
+ overlayAnchor.row + overlay.row - 1,
209
+ overlay.cols,
210
+ overlay.rows,
211
+ );
212
+ const resolvedRect =
213
+ overlay.clipToViewport === false
214
+ ? absoluteRect
215
+ : intersectRect(absoluteRect, layoutBase.viewport);
216
+ if (resolvedRect === null) {
217
+ continue;
218
+ }
219
+ layers.push({
220
+ id: overlay.id,
221
+ kind: 'overlay',
222
+ zIndex: overlay.zIndex ?? DEFAULT_OVERLAY_LAYER_Z_INDEX,
223
+ rect: resolvedRect,
224
+ });
225
+ }
226
+
227
+ const sortedLayers = layers.toSorted((left, right) => {
228
+ if (left.zIndex !== right.zIndex) {
229
+ return left.zIndex - right.zIndex;
230
+ }
231
+ return left.id.localeCompare(right.id);
232
+ });
233
+
234
+ return {
235
+ ...layoutBase,
236
+ layers: sortedLayers,
237
+ };
238
+ }
@@ -0,0 +1,222 @@
1
+ import type { UiModalOverlay, UiModalTheme } from './kit.ts';
2
+ import type {
3
+ ApiKeyPromptState,
4
+ CommandMenuActionDescriptor,
5
+ CommandMenuState,
6
+ ConversationTitleEditState,
7
+ NewThreadPromptState,
8
+ RepositoryPromptState,
9
+ TaskEditorPromptState,
10
+ } from './interaction/input.ts';
11
+
12
+ type AddDirectoryPromptState = { value: string; error: string | null };
13
+ type ModalOverlay = UiModalOverlay;
14
+ type ModalTheme = Partial<UiModalTheme>;
15
+
16
+ interface ModalManagerOptions {
17
+ readonly theme: ModalTheme;
18
+ readonly resolveRepositoryName: (repositoryId: string) => string | null;
19
+ readonly getCommandMenu: () => CommandMenuState | null;
20
+ readonly resolveCommandMenuActions: () => readonly CommandMenuActionDescriptor[];
21
+ readonly getNewThreadPrompt: () => NewThreadPromptState | null;
22
+ readonly getAddDirectoryPrompt: () => AddDirectoryPromptState | null;
23
+ readonly getApiKeyPrompt?: () => ApiKeyPromptState | null;
24
+ readonly getTaskEditorPrompt: () => TaskEditorPromptState | null;
25
+ readonly getRepositoryPrompt: () => RepositoryPromptState | null;
26
+ readonly getConversationTitleEdit: () => ConversationTitleEditState | null;
27
+ }
28
+
29
+ export interface ModalDismissOnOutsideClickInput {
30
+ readonly input: Buffer;
31
+ readonly inputRemainder: string;
32
+ readonly dismiss: () => void;
33
+ readonly buildCurrentModalOverlay: () => ModalOverlay | null;
34
+ readonly onInsidePointerPress?: (col: number, row: number) => boolean;
35
+ readonly isOverlayHit: (overlay: ModalOverlay, col: number, row: number) => boolean;
36
+ }
37
+
38
+ export interface ModalDismissOnOutsideClickResult {
39
+ readonly handled: boolean;
40
+ readonly inputRemainder: string;
41
+ }
42
+
43
+ export interface ModalManagerStrategies {
44
+ buildCommandMenuModalOverlay(
45
+ layoutCols: number,
46
+ viewportRows: number,
47
+ menu: CommandMenuState | null,
48
+ actions: readonly CommandMenuActionDescriptor[],
49
+ theme: ModalTheme,
50
+ ): ModalOverlay | null;
51
+ buildNewThreadModalOverlay(
52
+ layoutCols: number,
53
+ viewportRows: number,
54
+ prompt: NewThreadPromptState | null,
55
+ theme: ModalTheme,
56
+ ): ModalOverlay | null;
57
+ buildAddDirectoryModalOverlay(
58
+ layoutCols: number,
59
+ viewportRows: number,
60
+ prompt: AddDirectoryPromptState | null,
61
+ theme: ModalTheme,
62
+ ): ModalOverlay | null;
63
+ buildTaskEditorModalOverlay(
64
+ layoutCols: number,
65
+ viewportRows: number,
66
+ prompt: TaskEditorPromptState | null,
67
+ resolveRepositoryName: (repositoryId: string) => string | null,
68
+ theme: ModalTheme,
69
+ ): ModalOverlay | null;
70
+ buildApiKeyModalOverlay(
71
+ layoutCols: number,
72
+ viewportRows: number,
73
+ prompt: ApiKeyPromptState | null,
74
+ theme: ModalTheme,
75
+ ): ModalOverlay | null;
76
+ buildRepositoryModalOverlay(
77
+ layoutCols: number,
78
+ viewportRows: number,
79
+ prompt: RepositoryPromptState | null,
80
+ theme: ModalTheme,
81
+ ): ModalOverlay | null;
82
+ buildConversationTitleModalOverlay(
83
+ layoutCols: number,
84
+ viewportRows: number,
85
+ edit: ConversationTitleEditState | null,
86
+ theme: ModalTheme,
87
+ ): ModalOverlay | null;
88
+ dismissModalOnOutsideClick(
89
+ input: ModalDismissOnOutsideClickInput,
90
+ ): ModalDismissOnOutsideClickResult;
91
+ isOverlayHit(overlay: ModalOverlay, col: number, row: number): boolean;
92
+ }
93
+
94
+ interface ModalDismissInput {
95
+ readonly input: Buffer;
96
+ readonly inputRemainder: string;
97
+ readonly layoutCols: number;
98
+ readonly viewportRows: number;
99
+ readonly dismiss: () => void;
100
+ readonly onInsidePointerPress?: (col: number, row: number) => boolean;
101
+ }
102
+
103
+ interface ModalDismissResult {
104
+ readonly handled: boolean;
105
+ readonly inputRemainder: string;
106
+ }
107
+
108
+ export class ModalManager {
109
+ constructor(
110
+ private readonly options: ModalManagerOptions,
111
+ private readonly strategies: ModalManagerStrategies,
112
+ ) {}
113
+
114
+ buildCommandMenuOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
115
+ return this.strategies.buildCommandMenuModalOverlay(
116
+ layoutCols,
117
+ viewportRows,
118
+ this.options.getCommandMenu(),
119
+ this.options.resolveCommandMenuActions(),
120
+ this.options.theme,
121
+ );
122
+ }
123
+
124
+ buildNewThreadOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
125
+ return this.strategies.buildNewThreadModalOverlay(
126
+ layoutCols,
127
+ viewportRows,
128
+ this.options.getNewThreadPrompt(),
129
+ this.options.theme,
130
+ );
131
+ }
132
+
133
+ buildAddDirectoryOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
134
+ return this.strategies.buildAddDirectoryModalOverlay(
135
+ layoutCols,
136
+ viewportRows,
137
+ this.options.getAddDirectoryPrompt(),
138
+ this.options.theme,
139
+ );
140
+ }
141
+
142
+ buildTaskEditorOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
143
+ return this.strategies.buildTaskEditorModalOverlay(
144
+ layoutCols,
145
+ viewportRows,
146
+ this.options.getTaskEditorPrompt(),
147
+ this.options.resolveRepositoryName,
148
+ this.options.theme,
149
+ );
150
+ }
151
+
152
+ buildApiKeyOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
153
+ return this.strategies.buildApiKeyModalOverlay(
154
+ layoutCols,
155
+ viewportRows,
156
+ this.options.getApiKeyPrompt?.() ?? null,
157
+ this.options.theme,
158
+ );
159
+ }
160
+
161
+ buildRepositoryOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
162
+ return this.strategies.buildRepositoryModalOverlay(
163
+ layoutCols,
164
+ viewportRows,
165
+ this.options.getRepositoryPrompt(),
166
+ this.options.theme,
167
+ );
168
+ }
169
+
170
+ buildConversationTitleOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
171
+ return this.strategies.buildConversationTitleModalOverlay(
172
+ layoutCols,
173
+ viewportRows,
174
+ this.options.getConversationTitleEdit(),
175
+ this.options.theme,
176
+ );
177
+ }
178
+
179
+ buildCurrentOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
180
+ const commandMenuOverlay = this.buildCommandMenuOverlay(layoutCols, viewportRows);
181
+ if (commandMenuOverlay !== null) {
182
+ return commandMenuOverlay;
183
+ }
184
+ const newThreadOverlay = this.buildNewThreadOverlay(layoutCols, viewportRows);
185
+ if (newThreadOverlay !== null) {
186
+ return newThreadOverlay;
187
+ }
188
+ const addDirectoryOverlay = this.buildAddDirectoryOverlay(layoutCols, viewportRows);
189
+ if (addDirectoryOverlay !== null) {
190
+ return addDirectoryOverlay;
191
+ }
192
+ const taskEditorOverlay = this.buildTaskEditorOverlay(layoutCols, viewportRows);
193
+ if (taskEditorOverlay !== null) {
194
+ return taskEditorOverlay;
195
+ }
196
+ const apiKeyOverlay = this.buildApiKeyOverlay(layoutCols, viewportRows);
197
+ if (apiKeyOverlay !== null) {
198
+ return apiKeyOverlay;
199
+ }
200
+ const repositoryOverlay = this.buildRepositoryOverlay(layoutCols, viewportRows);
201
+ if (repositoryOverlay !== null) {
202
+ return repositoryOverlay;
203
+ }
204
+ return this.buildConversationTitleOverlay(layoutCols, viewportRows);
205
+ }
206
+
207
+ dismissOnOutsideClick(input: ModalDismissInput): ModalDismissResult {
208
+ return this.strategies.dismissModalOnOutsideClick({
209
+ input: input.input,
210
+ inputRemainder: input.inputRemainder,
211
+ dismiss: input.dismiss,
212
+ buildCurrentModalOverlay: () =>
213
+ this.buildCurrentOverlay(input.layoutCols, input.viewportRows),
214
+ isOverlayHit: this.strategies.isOverlayHit,
215
+ ...(input.onInsidePointerPress === undefined
216
+ ? {}
217
+ : {
218
+ onInsidePointerPress: input.onInsidePointerPress,
219
+ }),
220
+ });
221
+ }
222
+ }
@@ -1,11 +1,12 @@
1
- import { diffRenderedRows } from '../mux/dual-pane-core.ts';
2
- import { cursorStyleEqual, cursorStyleToDecscusr } from '../mux/render-frame.ts';
3
- import { findAnsiIntegrityIssues } from '../mux/ansi-integrity.ts';
1
+ import {
2
+ cursorStyleEqual,
3
+ cursorStyleToDecscusr,
4
+ diffRenderedRows,
5
+ findAnsiIntegrityIssues,
6
+ type RenderCursorStyle,
7
+ } from './frame-primitives.ts';
4
8
 
5
- export interface ScreenCursorStyle {
6
- readonly shape: 'block' | 'underline' | 'bar';
7
- readonly blinking: boolean;
8
- }
9
+ export type ScreenCursorStyle = RenderCursorStyle;
9
10
 
10
11
  interface ScreenLayout {
11
12
  readonly paneRows: number;
@@ -43,10 +44,33 @@ interface ScreenFlushResult {
43
44
  readonly shouldShowCursor: boolean;
44
45
  }
45
46
 
46
- interface ScreenDependencies {
47
- readonly writeOutput: (output: string) => void;
48
- readonly writeError: (output: string) => void;
49
- readonly findAnsiIssues: (rows: readonly string[]) => readonly string[];
47
+ export interface ScreenWriter {
48
+ writeOutput(output: string): void;
49
+ writeError(output: string): void;
50
+ }
51
+
52
+ export class ProcessScreenWriter implements ScreenWriter {
53
+ constructor() {}
54
+
55
+ writeOutput(output: string): void {
56
+ process.stdout.write(output);
57
+ }
58
+
59
+ writeError(output: string): void {
60
+ process.stderr.write(output);
61
+ }
62
+ }
63
+
64
+ export interface ScreenAnsiValidator {
65
+ findIssues(rows: readonly string[]): readonly string[];
66
+ }
67
+
68
+ export class DefaultScreenAnsiValidator implements ScreenAnsiValidator {
69
+ constructor() {}
70
+
71
+ findIssues(rows: readonly string[]): readonly string[] {
72
+ return findAnsiIntegrityIssues(rows);
73
+ }
50
74
  }
51
75
 
52
76
  const TERMINAL_SYNC_UPDATE_BEGIN = '\u001b[?2026h';
@@ -66,7 +90,17 @@ function mergeUniqueRows(left: readonly number[], right: readonly number[]): rea
66
90
  for (const row of right) {
67
91
  merged.add(row);
68
92
  }
69
- return [...merged].sort((a, b) => a - b);
93
+ const output = [...merged];
94
+ for (let index = 1; index < output.length; index += 1) {
95
+ const value = output[index]!;
96
+ let insertIndex = index - 1;
97
+ while (insertIndex >= 0 && output[insertIndex]! > value) {
98
+ output[insertIndex + 1] = output[insertIndex]!;
99
+ insertIndex -= 1;
100
+ }
101
+ output[insertIndex + 1] = value;
102
+ }
103
+ return output;
70
104
  }
71
105
 
72
106
  export class Screen {
@@ -79,15 +113,10 @@ export class Screen {
79
113
  private renderedBracketedPaste: boolean | null = null;
80
114
  private ansiValidationReported = false;
81
115
 
82
- private readonly deps: ScreenDependencies;
83
-
84
- constructor(deps?: Partial<ScreenDependencies>) {
85
- this.deps = {
86
- writeOutput: deps?.writeOutput ?? ((output) => process.stdout.write(output)),
87
- writeError: deps?.writeError ?? ((output) => process.stderr.write(output)),
88
- findAnsiIssues: deps?.findAnsiIssues ?? findAnsiIntegrityIssues,
89
- };
90
- }
116
+ constructor(
117
+ private readonly writer: ScreenWriter = new ProcessScreenWriter(),
118
+ private readonly ansiValidator: ScreenAnsiValidator = new DefaultScreenAnsiValidator(),
119
+ ) {}
91
120
 
92
121
  isDirty(): boolean {
93
122
  return this.dirty;
@@ -116,10 +145,10 @@ export class Screen {
116
145
  }
117
146
 
118
147
  if (input.validateAnsi) {
119
- const issues = this.deps.findAnsiIssues(input.rows);
148
+ const issues = this.ansiValidator.findIssues(input.rows);
120
149
  if (issues.length > 0 && !this.ansiValidationReported) {
121
150
  this.ansiValidationReported = true;
122
- this.deps.writeError(`[mux] ansi-integrity-failed ${issues.join(' | ')}\n`);
151
+ this.writer.writeError(`[mux] ansi-integrity-failed ${issues.join(' | ')}\n`);
123
152
  }
124
153
  }
125
154
 
@@ -163,7 +192,6 @@ export class Screen {
163
192
  }
164
193
 
165
194
  output += input.selectionOverlay;
166
-
167
195
  shouldShowCursor =
168
196
  input.rightFrame.viewport.followOutput &&
169
197
  input.rightFrame.cursor.visible &&
@@ -194,13 +222,12 @@ export class Screen {
194
222
  }
195
223
 
196
224
  if (output.length > 0) {
197
- this.deps.writeOutput(`${TERMINAL_SYNC_UPDATE_BEGIN}${output}${TERMINAL_SYNC_UPDATE_END}`);
225
+ this.writer.writeOutput(`${TERMINAL_SYNC_UPDATE_BEGIN}${output}${TERMINAL_SYNC_UPDATE_END}`);
198
226
  }
199
227
 
200
228
  this.previousRows = diff.nextRows;
201
229
  this.previousSelectionRows = input.selectionRows;
202
230
  this.dirty = false;
203
-
204
231
  return {
205
232
  wroteOutput: output.length > 0,
206
233
  changedRowCount: diff.changedRows.length,