@jmoyers/harness 0.1.10 → 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 (239) hide show
  1. package/README.md +31 -35
  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/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
  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 -3721
  38. package/scripts/control-plane-daemon.ts +24 -2
  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 -3007
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/default-gateway-pointer.ts +193 -0
  46. package/src/cli/gateway/runtime.ts +1872 -0
  47. package/src/cli/parsing/flags.ts +23 -0
  48. package/src/cli/parsing/session.ts +42 -0
  49. package/src/cli/runtime/context.ts +193 -0
  50. package/src/cli/runtime-app/application.ts +392 -0
  51. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  52. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  53. package/src/cli/workflows/runtime.ts +965 -0
  54. package/src/clients/tui/left-rail-interactions.ts +519 -0
  55. package/src/clients/tui/main-pane-interactions.ts +509 -0
  56. package/src/clients/tui/modal-input-routing.ts +71 -0
  57. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  58. package/src/clients/web/synced-selectors.ts +132 -0
  59. package/src/codex/live-session.ts +82 -29
  60. package/src/config/config-core.ts +361 -10
  61. package/src/config/harness-paths.ts +4 -7
  62. package/src/config/harness-runtime-migration.ts +142 -19
  63. package/src/config/harness.config.template.jsonc +33 -0
  64. package/src/config/secrets-core.ts +92 -4
  65. package/src/control-plane/agent-realtime-api.ts +82 -427
  66. package/src/control-plane/prompt/thread-title-namer.ts +49 -23
  67. package/src/control-plane/session-summary.ts +10 -81
  68. package/src/control-plane/status/reducer-base.ts +12 -12
  69. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  70. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  71. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  72. package/src/control-plane/stream-client.ts +12 -2
  73. package/src/control-plane/stream-command-parser.ts +83 -143
  74. package/src/control-plane/stream-protocol.ts +53 -37
  75. package/src/control-plane/stream-server-background.ts +18 -2
  76. package/src/control-plane/stream-server-command.ts +376 -69
  77. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  78. package/src/control-plane/stream-server.ts +943 -80
  79. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  80. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  81. package/src/core/state/observed-stream-cursor.ts +43 -0
  82. package/src/core/state/synced-observed-state.ts +273 -0
  83. package/src/core/store/harness-synced-store.ts +81 -0
  84. package/src/diff/budget.ts +136 -0
  85. package/src/diff/build.ts +289 -0
  86. package/src/diff/chunker.ts +146 -0
  87. package/src/diff/git-invoke.ts +315 -0
  88. package/src/diff/git-parse.ts +472 -0
  89. package/src/diff/hash.ts +70 -0
  90. package/src/diff/index.ts +24 -0
  91. package/src/diff/normalize.ts +134 -0
  92. package/src/diff/types.ts +178 -0
  93. package/src/diff-ui/args.ts +346 -0
  94. package/src/diff-ui/commands.ts +123 -0
  95. package/src/diff-ui/finder.ts +94 -0
  96. package/src/diff-ui/highlight.ts +127 -0
  97. package/src/diff-ui/index.ts +2 -0
  98. package/src/diff-ui/model.ts +141 -0
  99. package/src/diff-ui/pager.ts +412 -0
  100. package/src/diff-ui/render.ts +337 -0
  101. package/src/diff-ui/runtime.ts +379 -0
  102. package/src/diff-ui/state.ts +224 -0
  103. package/src/diff-ui/types.ts +236 -0
  104. package/src/domain/conversations.ts +11 -7
  105. package/src/domain/workspace.ts +76 -4
  106. package/src/mux/control-plane-op-queue.ts +93 -7
  107. package/src/mux/conversation-rail.ts +28 -71
  108. package/src/mux/dual-pane-core.ts +13 -13
  109. package/src/mux/harness-core-ui.ts +313 -42
  110. package/src/mux/input-shortcuts.ts +22 -112
  111. package/src/mux/keybinding-catalog.ts +340 -0
  112. package/src/mux/keybinding-registry.ts +103 -0
  113. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  114. package/src/mux/live-mux/command-menu.ts +167 -4
  115. package/src/mux/live-mux/conversation-state.ts +13 -0
  116. package/src/mux/live-mux/directory-resolution.ts +1 -1
  117. package/src/mux/live-mux/git-parsing.ts +16 -0
  118. package/src/mux/live-mux/git-snapshot.ts +33 -2
  119. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  120. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  121. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  122. package/src/mux/live-mux/input-forwarding.ts +59 -2
  123. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  124. package/src/mux/live-mux/left-nav.ts +35 -0
  125. package/src/mux/live-mux/link-click.ts +292 -0
  126. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  127. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  128. package/src/mux/live-mux/modal-input-reducers.ts +106 -8
  129. package/src/mux/live-mux/modal-overlays.ts +210 -31
  130. package/src/mux/live-mux/modal-pointer.ts +3 -7
  131. package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
  132. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  133. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  134. package/src/mux/live-mux/pointer-routing.ts +5 -2
  135. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  136. package/src/mux/live-mux/rail-layout.ts +33 -30
  137. package/src/mux/live-mux/release-notes.ts +383 -0
  138. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  139. package/src/mux/live-mux/repository-folding.ts +3 -0
  140. package/src/mux/live-mux/selection.ts +0 -4
  141. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  142. package/src/mux/project-pane-github-review.ts +271 -0
  143. package/src/mux/render-frame.ts +4 -0
  144. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  145. package/src/mux/task-composer.ts +21 -14
  146. package/src/mux/task-focused-pane.ts +118 -117
  147. package/src/mux/task-screen-keybindings.ts +19 -82
  148. package/src/mux/workspace-rail-model.ts +270 -104
  149. package/src/mux/workspace-rail.ts +45 -22
  150. package/src/pty/session-broker.ts +1 -1
  151. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  152. package/src/services/control-plane.ts +50 -32
  153. package/src/services/conversation-lifecycle.ts +118 -87
  154. package/src/services/conversation-startup-hydration.ts +20 -12
  155. package/src/services/directory-hydration.ts +21 -16
  156. package/src/services/event-persistence.ts +7 -0
  157. package/src/services/left-rail-pointer-handler.ts +329 -0
  158. package/src/services/mux-ui-state-persistence.ts +5 -1
  159. package/src/services/recording.ts +34 -26
  160. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  161. package/src/services/runtime-control-actions.ts +79 -61
  162. package/src/services/runtime-control-plane-ops.ts +122 -83
  163. package/src/services/runtime-conversation-actions.ts +40 -26
  164. package/src/services/runtime-conversation-activation.ts +82 -30
  165. package/src/services/runtime-conversation-starter.ts +80 -48
  166. package/src/services/runtime-conversation-title-edit.ts +91 -80
  167. package/src/services/runtime-envelope-handler.ts +107 -105
  168. package/src/services/runtime-git-state.ts +42 -29
  169. package/src/services/runtime-layout-resize.ts +3 -1
  170. package/src/services/runtime-left-rail-render.ts +99 -63
  171. package/src/services/runtime-nim-cli-session.ts +438 -0
  172. package/src/services/runtime-nim-session.ts +705 -0
  173. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  174. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  175. package/src/services/runtime-process-wiring.ts +29 -36
  176. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  177. package/src/services/runtime-render-flush.ts +63 -70
  178. package/src/services/runtime-render-lifecycle.ts +65 -64
  179. package/src/services/runtime-render-orchestrator.ts +55 -45
  180. package/src/services/runtime-render-pipeline.ts +106 -103
  181. package/src/services/runtime-render-state.ts +62 -49
  182. package/src/services/runtime-repository-actions.ts +97 -70
  183. package/src/services/runtime-right-pane-render.ts +80 -53
  184. package/src/services/runtime-shutdown.ts +38 -35
  185. package/src/services/runtime-stream-subscriptions.ts +35 -27
  186. package/src/services/runtime-task-composer-persistence.ts +71 -59
  187. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  188. package/src/services/runtime-task-editor-actions.ts +46 -29
  189. package/src/services/runtime-task-pane-actions.ts +220 -134
  190. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  191. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  192. package/src/services/runtime-workspace-observed-events.ts +33 -184
  193. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  194. package/src/services/session-diagnostics-store.ts +217 -0
  195. package/src/services/startup-background-resume.ts +26 -21
  196. package/src/services/startup-orchestrator.ts +16 -13
  197. package/src/services/startup-paint-tracker.ts +29 -21
  198. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  199. package/src/services/startup-settled-gate.ts +25 -15
  200. package/src/services/startup-shutdown.ts +18 -22
  201. package/src/services/startup-state-hydration.ts +44 -34
  202. package/src/services/startup-visibility.ts +12 -4
  203. package/src/services/task-pane-selection-actions.ts +89 -72
  204. package/src/services/task-planning-hydration.ts +24 -18
  205. package/src/services/task-planning-observed-events.ts +50 -52
  206. package/src/services/workspace-observed-events.ts +66 -63
  207. package/src/storage/storage-lifecycle-core.ts +438 -0
  208. package/src/store/control-plane-store-normalize.ts +33 -242
  209. package/src/store/control-plane-store-types.ts +1 -35
  210. package/src/store/control-plane-store.ts +396 -56
  211. package/src/store/event-store.ts +397 -3
  212. package/src/terminal/snapshot-oracle.ts +207 -94
  213. package/src/ui/mux-theme.ts +112 -8
  214. package/src/ui/panes/home-gridfire.ts +40 -31
  215. package/src/ui/panes/home.ts +10 -2
  216. package/src/ui/panes/nim.ts +315 -0
  217. package/src/mux/live-mux/actions-task.ts +0 -115
  218. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  219. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
  220. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  221. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  222. package/src/services/runtime-directory-actions.ts +0 -164
  223. package/src/services/runtime-input-pipeline.ts +0 -50
  224. package/src/services/runtime-input-router.ts +0 -189
  225. package/src/services/runtime-main-pane-input.ts +0 -230
  226. package/src/services/runtime-modal-input.ts +0 -119
  227. package/src/services/runtime-navigation-input.ts +0 -197
  228. package/src/services/runtime-rail-input.ts +0 -278
  229. package/src/services/runtime-task-pane.ts +0 -62
  230. package/src/services/runtime-workspace-actions.ts +0 -158
  231. package/src/ui/conversation-input-forwarder.ts +0 -114
  232. package/src/ui/conversation-selection-input.ts +0 -103
  233. package/src/ui/global-shortcut-input.ts +0 -89
  234. package/src/ui/input.ts +0 -238
  235. package/src/ui/kit.ts +0 -509
  236. package/src/ui/left-nav-input.ts +0 -80
  237. package/src/ui/left-rail-pointer-input.ts +0 -148
  238. package/src/ui/repository-fold-input.ts +0 -91
  239. 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
+ }
@@ -1,29 +1,17 @@
1
- import {
2
- buildCommandMenuModalOverlay as buildCommandMenuModalOverlayFrame,
3
- buildAddDirectoryModalOverlay as buildAddDirectoryModalOverlayFrame,
4
- buildConversationTitleModalOverlay as buildConversationTitleModalOverlayFrame,
5
- buildNewThreadModalOverlay as buildNewThreadModalOverlayFrame,
6
- buildRepositoryModalOverlay as buildRepositoryModalOverlayFrame,
7
- buildTaskEditorModalOverlay as buildTaskEditorModalOverlayFrame,
8
- } from '../../mux/live-mux/modal-overlays.ts';
9
- import { dismissModalOnOutsideClick as dismissModalOnOutsideClickFrame } from '../../mux/live-mux/modal-pointer.ts';
1
+ import type { UiModalOverlay, UiModalTheme } from './kit.ts';
10
2
  import type {
3
+ ApiKeyPromptState,
11
4
  CommandMenuActionDescriptor,
12
5
  CommandMenuState,
13
- } from '../../mux/live-mux/command-menu.ts';
14
- import type { createNewThreadPromptState } from '../../mux/new-thread-prompt.ts';
15
- import type {
16
6
  ConversationTitleEditState,
7
+ NewThreadPromptState,
17
8
  RepositoryPromptState,
18
9
  TaskEditorPromptState,
19
- } from '../../domain/workspace.ts';
20
- import { isUiModalOverlayHit } from '../kit.ts';
10
+ } from './interaction/input.ts';
21
11
 
22
- type NewThreadPromptState = ReturnType<typeof createNewThreadPromptState>;
23
12
  type AddDirectoryPromptState = { value: string; error: string | null };
24
- type ModalOverlay = Exclude<ReturnType<typeof buildNewThreadModalOverlayFrame>, null>;
25
- type ModalTheme = Parameters<typeof buildNewThreadModalOverlayFrame>[3];
26
- type DismissModalOnOutsideClickInput = Parameters<typeof dismissModalOnOutsideClickFrame>[0];
13
+ type ModalOverlay = UiModalOverlay;
14
+ type ModalTheme = Partial<UiModalTheme>;
27
15
 
28
16
  interface ModalManagerOptions {
29
17
  readonly theme: ModalTheme;
@@ -32,20 +20,75 @@ interface ModalManagerOptions {
32
20
  readonly resolveCommandMenuActions: () => readonly CommandMenuActionDescriptor[];
33
21
  readonly getNewThreadPrompt: () => NewThreadPromptState | null;
34
22
  readonly getAddDirectoryPrompt: () => AddDirectoryPromptState | null;
23
+ readonly getApiKeyPrompt?: () => ApiKeyPromptState | null;
35
24
  readonly getTaskEditorPrompt: () => TaskEditorPromptState | null;
36
25
  readonly getRepositoryPrompt: () => RepositoryPromptState | null;
37
26
  readonly getConversationTitleEdit: () => ConversationTitleEditState | null;
38
27
  }
39
28
 
40
- interface ModalManagerDependencies {
41
- readonly buildCommandMenuModalOverlay?: typeof buildCommandMenuModalOverlayFrame;
42
- readonly buildNewThreadModalOverlay?: typeof buildNewThreadModalOverlayFrame;
43
- readonly buildAddDirectoryModalOverlay?: typeof buildAddDirectoryModalOverlayFrame;
44
- readonly buildTaskEditorModalOverlay?: typeof buildTaskEditorModalOverlayFrame;
45
- readonly buildRepositoryModalOverlay?: typeof buildRepositoryModalOverlayFrame;
46
- readonly buildConversationTitleModalOverlay?: typeof buildConversationTitleModalOverlayFrame;
47
- readonly dismissModalOnOutsideClick?: typeof dismissModalOnOutsideClickFrame;
48
- readonly isOverlayHit?: typeof isUiModalOverlayHit;
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;
49
92
  }
50
93
 
51
94
  interface ModalDismissInput {
@@ -63,38 +106,13 @@ interface ModalDismissResult {
63
106
  }
64
107
 
65
108
  export class ModalManager {
66
- private readonly buildCommandMenuModalOverlay: typeof buildCommandMenuModalOverlayFrame;
67
- private readonly buildNewThreadModalOverlay: typeof buildNewThreadModalOverlayFrame;
68
- private readonly buildAddDirectoryModalOverlay: typeof buildAddDirectoryModalOverlayFrame;
69
- private readonly buildTaskEditorModalOverlay: typeof buildTaskEditorModalOverlayFrame;
70
- private readonly buildRepositoryModalOverlay: typeof buildRepositoryModalOverlayFrame;
71
- private readonly buildConversationTitleModalOverlay: typeof buildConversationTitleModalOverlayFrame;
72
- private readonly dismissModalOnOutsideClick: typeof dismissModalOnOutsideClickFrame;
73
- private readonly isOverlayHit: typeof isUiModalOverlayHit;
74
-
75
109
  constructor(
76
110
  private readonly options: ModalManagerOptions,
77
- dependencies: ModalManagerDependencies = {},
78
- ) {
79
- this.buildCommandMenuModalOverlay =
80
- dependencies.buildCommandMenuModalOverlay ?? buildCommandMenuModalOverlayFrame;
81
- this.buildNewThreadModalOverlay =
82
- dependencies.buildNewThreadModalOverlay ?? buildNewThreadModalOverlayFrame;
83
- this.buildAddDirectoryModalOverlay =
84
- dependencies.buildAddDirectoryModalOverlay ?? buildAddDirectoryModalOverlayFrame;
85
- this.buildTaskEditorModalOverlay =
86
- dependencies.buildTaskEditorModalOverlay ?? buildTaskEditorModalOverlayFrame;
87
- this.buildRepositoryModalOverlay =
88
- dependencies.buildRepositoryModalOverlay ?? buildRepositoryModalOverlayFrame;
89
- this.buildConversationTitleModalOverlay =
90
- dependencies.buildConversationTitleModalOverlay ?? buildConversationTitleModalOverlayFrame;
91
- this.dismissModalOnOutsideClick =
92
- dependencies.dismissModalOnOutsideClick ?? dismissModalOnOutsideClickFrame;
93
- this.isOverlayHit = dependencies.isOverlayHit ?? isUiModalOverlayHit;
94
- }
111
+ private readonly strategies: ModalManagerStrategies,
112
+ ) {}
95
113
 
96
114
  buildCommandMenuOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
97
- return this.buildCommandMenuModalOverlay(
115
+ return this.strategies.buildCommandMenuModalOverlay(
98
116
  layoutCols,
99
117
  viewportRows,
100
118
  this.options.getCommandMenu(),
@@ -104,7 +122,7 @@ export class ModalManager {
104
122
  }
105
123
 
106
124
  buildNewThreadOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
107
- return this.buildNewThreadModalOverlay(
125
+ return this.strategies.buildNewThreadModalOverlay(
108
126
  layoutCols,
109
127
  viewportRows,
110
128
  this.options.getNewThreadPrompt(),
@@ -113,7 +131,7 @@ export class ModalManager {
113
131
  }
114
132
 
115
133
  buildAddDirectoryOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
116
- return this.buildAddDirectoryModalOverlay(
134
+ return this.strategies.buildAddDirectoryModalOverlay(
117
135
  layoutCols,
118
136
  viewportRows,
119
137
  this.options.getAddDirectoryPrompt(),
@@ -122,7 +140,7 @@ export class ModalManager {
122
140
  }
123
141
 
124
142
  buildTaskEditorOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
125
- return this.buildTaskEditorModalOverlay(
143
+ return this.strategies.buildTaskEditorModalOverlay(
126
144
  layoutCols,
127
145
  viewportRows,
128
146
  this.options.getTaskEditorPrompt(),
@@ -131,8 +149,17 @@ export class ModalManager {
131
149
  );
132
150
  }
133
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
+
134
161
  buildRepositoryOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
135
- return this.buildRepositoryModalOverlay(
162
+ return this.strategies.buildRepositoryModalOverlay(
136
163
  layoutCols,
137
164
  viewportRows,
138
165
  this.options.getRepositoryPrompt(),
@@ -141,7 +168,7 @@ export class ModalManager {
141
168
  }
142
169
 
143
170
  buildConversationTitleOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
144
- return this.buildConversationTitleModalOverlay(
171
+ return this.strategies.buildConversationTitleModalOverlay(
145
172
  layoutCols,
146
173
  viewportRows,
147
174
  this.options.getConversationTitleEdit(),
@@ -166,6 +193,10 @@ export class ModalManager {
166
193
  if (taskEditorOverlay !== null) {
167
194
  return taskEditorOverlay;
168
195
  }
196
+ const apiKeyOverlay = this.buildApiKeyOverlay(layoutCols, viewportRows);
197
+ if (apiKeyOverlay !== null) {
198
+ return apiKeyOverlay;
199
+ }
169
200
  const repositoryOverlay = this.buildRepositoryOverlay(layoutCols, viewportRows);
170
201
  if (repositoryOverlay !== null) {
171
202
  return repositoryOverlay;
@@ -174,19 +205,18 @@ export class ModalManager {
174
205
  }
175
206
 
176
207
  dismissOnOutsideClick(input: ModalDismissInput): ModalDismissResult {
177
- const dismissInput: DismissModalOnOutsideClickInput = {
208
+ return this.strategies.dismissModalOnOutsideClick({
178
209
  input: input.input,
179
210
  inputRemainder: input.inputRemainder,
180
211
  dismiss: input.dismiss,
181
212
  buildCurrentModalOverlay: () =>
182
213
  this.buildCurrentOverlay(input.layoutCols, input.viewportRows),
183
- isOverlayHit: this.isOverlayHit,
214
+ isOverlayHit: this.strategies.isOverlayHit,
184
215
  ...(input.onInsidePointerPress === undefined
185
216
  ? {}
186
217
  : {
187
218
  onInsidePointerPress: input.onInsidePointerPress,
188
219
  }),
189
- };
190
- return this.dismissModalOnOutsideClick(dismissInput);
220
+ });
191
221
  }
192
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,