@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
@@ -1,5 +1,5 @@
1
1
  import { measureDisplayWidth } from '../../terminal/snapshot-oracle.ts';
2
- import { createUiSurface, renderUiSurfaceAnsiRows, type UiStyle } from '../surface.ts';
2
+ import { SurfaceBuffer, type UiStyle } from '../../../packages/harness-ui/src/surface.ts';
3
3
 
4
4
  interface HomeGridfireOptions {
5
5
  readonly cols: number;
@@ -8,6 +8,7 @@ interface HomeGridfireOptions {
8
8
  readonly timeMs: number;
9
9
  readonly overlayTitle: string | null;
10
10
  readonly overlaySubtitle: string | null;
11
+ readonly overlayPlacement?: 'center' | 'bottom';
11
12
  }
12
13
 
13
14
  type RgbTriplet = readonly [number, number, number];
@@ -26,25 +27,25 @@ const GRID_CHARS = {
26
27
  } as const;
27
28
 
28
29
  const FG_PALETTE: readonly RgbTriplet[] = [
29
- [14, 11, 28],
30
- [28, 20, 55],
31
- [50, 30, 100],
32
- [75, 45, 155],
33
- [55, 85, 200],
34
- [90, 140, 240],
35
- [150, 190, 255],
36
- [220, 235, 255],
30
+ [8, 20, 24],
31
+ [10, 35, 42],
32
+ [14, 58, 70],
33
+ [20, 86, 105],
34
+ [28, 118, 146],
35
+ [42, 154, 188],
36
+ [90, 198, 220],
37
+ [180, 236, 244],
37
38
  ];
38
39
 
39
40
  const BG_PALETTE: readonly RgbTriplet[] = [
40
- [8, 6, 16],
41
- [12, 10, 22],
42
- [16, 13, 30],
43
- [20, 16, 38],
44
- [18, 20, 45],
45
- [16, 24, 50],
46
- [20, 28, 55],
47
- [24, 32, 58],
41
+ [2, 10, 12],
42
+ [4, 16, 18],
43
+ [6, 22, 24],
44
+ [8, 28, 30],
45
+ [10, 34, 36],
46
+ [12, 42, 44],
47
+ [14, 50, 54],
48
+ [18, 62, 66],
48
49
  ];
49
50
 
50
51
  function lerp(start: number, end: number, t: number): number {
@@ -90,7 +91,7 @@ function styleFromColors(fg: RgbTriplet, bg: RgbTriplet, bold = false): UiStyle
90
91
  }
91
92
 
92
93
  function writeGlyph(
93
- surface: ReturnType<typeof createUiSurface>,
94
+ surface: SurfaceBuffer,
94
95
  row: number,
95
96
  col: number,
96
97
  glyph: string,
@@ -198,7 +199,7 @@ function pickGridGlyph(
198
199
  : GRID_CHARS.empty;
199
200
  }
200
201
 
201
- function paintBackground(surface: ReturnType<typeof createUiSurface>, phase: number): void {
202
+ function paintBackground(surface: SurfaceBuffer, phase: number): void {
202
203
  for (let row = 0; row < surface.rows; row += 1) {
203
204
  for (let col = 0; col < surface.cols; col += 1) {
204
205
  const energy = gridEnergy(col, row, surface.cols, surface.rows, phase);
@@ -211,7 +212,7 @@ function paintBackground(surface: ReturnType<typeof createUiSurface>, phase: num
211
212
  }
212
213
 
213
214
  function paintOverlayTextRow(
214
- surface: ReturnType<typeof createUiSurface>,
215
+ surface: SurfaceBuffer,
215
216
  row: number,
216
217
  text: string,
217
218
  phase: number,
@@ -242,7 +243,7 @@ function paintOverlayTextRow(
242
243
  }
243
244
 
244
245
  function paintCenteredLabel(
245
- surface: ReturnType<typeof createUiSurface>,
246
+ surface: SurfaceBuffer,
246
247
  row: number,
247
248
  text: string | null,
248
249
  phase: number,
@@ -279,7 +280,7 @@ function paintCenteredLabel(
279
280
  export function renderHomeGridfireAnsiRows(options: HomeGridfireOptions): readonly string[] {
280
281
  const safeCols = Math.max(1, options.cols);
281
282
  const safeRows = Math.max(1, options.rows);
282
- const surface = createUiSurface(safeCols, safeRows);
283
+ const surface = new SurfaceBuffer(safeCols, safeRows);
283
284
  const phase = options.timeMs / 1000;
284
285
 
285
286
  paintBackground(surface, phase);
@@ -289,14 +290,22 @@ export function renderHomeGridfireAnsiRows(options: HomeGridfireOptions): readon
289
290
  paintOverlayTextRow(surface, row, line, phase);
290
291
  }
291
292
 
292
- const centerRow = Math.floor(safeRows / 2);
293
- paintCenteredLabel(surface, centerRow, options.overlayTitle, phase);
294
- paintCenteredLabel(
295
- surface,
296
- Math.min(safeRows - 1, centerRow + 2),
297
- options.overlaySubtitle,
298
- phase,
299
- );
293
+ const overlayPlacement = options.overlayPlacement ?? 'center';
294
+ if (overlayPlacement === 'bottom') {
295
+ const subtitleRow = Math.max(0, safeRows - 2);
296
+ const titleRow = Math.max(0, subtitleRow - 1);
297
+ paintCenteredLabel(surface, titleRow, options.overlayTitle, phase);
298
+ paintCenteredLabel(surface, subtitleRow, options.overlaySubtitle, phase);
299
+ } else {
300
+ const centerRow = Math.floor(safeRows / 2);
301
+ paintCenteredLabel(surface, centerRow, options.overlayTitle, phase);
302
+ paintCenteredLabel(
303
+ surface,
304
+ Math.min(safeRows - 1, centerRow + 2),
305
+ options.overlaySubtitle,
306
+ phase,
307
+ );
308
+ }
300
309
 
301
- return renderUiSurfaceAnsiRows(surface);
310
+ return surface.renderAnsiRows();
302
311
  }
@@ -29,6 +29,7 @@ interface HomePaneRenderInput {
29
29
  readonly layout: HomePaneLayout;
30
30
  readonly repositories: ReadonlyMap<string, TaskFocusedPaneRepositoryRecord>;
31
31
  readonly tasks: ReadonlyMap<string, TaskFocusedPaneTaskRecord>;
32
+ readonly showTaskPlanningUi?: boolean;
32
33
  readonly selectedRepositoryId: string | null;
33
34
  readonly repositoryDropdownOpen: boolean;
34
35
  readonly editorTarget: TaskFocusedPaneEditorTarget;
@@ -69,6 +70,7 @@ export class HomePane {
69
70
  const rows = Array.from({ length: safeRows }, () => blankRow);
70
71
  return {
71
72
  rows,
73
+ plainRows: rows,
72
74
  taskIds: Array.from({ length: safeRows }, () => null),
73
75
  repositoryIds: Array.from({ length: safeRows }, () => null),
74
76
  actions: Array.from({ length: safeRows }, () => null),
@@ -79,7 +81,10 @@ export class HomePane {
79
81
  }
80
82
 
81
83
  render(input: HomePaneRenderInput): TaskFocusedPaneView {
82
- const view = this.showTaskPlanningUi
84
+ const showTaskPlanningUi = input.showTaskPlanningUi ?? this.showTaskPlanningUi;
85
+ const nowMs = this.animateBackground ? this.nowMs() : this.staticBackgroundTimeMs;
86
+ const cursorVisible = Math.floor(nowMs / 530) % 2 === 0;
87
+ const view = showTaskPlanningUi
83
88
  ? this.renderTaskFocusedPaneView({
84
89
  repositories: input.repositories,
85
90
  tasks: input.tasks,
@@ -92,17 +97,20 @@ export class HomePane {
92
97
  cols: input.layout.rightCols,
93
98
  rows: input.layout.paneRows,
94
99
  scrollTop: input.scrollTop,
100
+ cursorVisible,
95
101
  })
96
102
  : this.hiddenTaskPlanningView(input.layout);
97
103
  return {
98
104
  ...view,
105
+ plainRows: view.rows,
99
106
  rows: this.renderBackgroundRows({
100
107
  cols: input.layout.rightCols,
101
108
  rows: input.layout.paneRows,
102
109
  contentRows: view.rows,
103
- timeMs: this.animateBackground ? this.nowMs() : this.staticBackgroundTimeMs,
110
+ timeMs: nowMs,
104
111
  overlayTitle: 'GSV Sleeper Service',
105
112
  overlaySubtitle: this.overlaySubtitle,
113
+ overlayPlacement: showTaskPlanningUi ? 'bottom' : 'center',
106
114
  }),
107
115
  };
108
116
  }
@@ -0,0 +1,315 @@
1
+ import {
2
+ DEFAULT_UI_STYLE,
3
+ SurfaceBuffer,
4
+ type UiColor,
5
+ type UiStyle,
6
+ } from '../../../packages/harness-ui/src/surface.ts';
7
+ import { UiKit } from '../../../packages/harness-ui/src/kit.ts';
8
+ import { measureDisplayWidth } from '../../terminal/snapshot-oracle.ts';
9
+ import { getActiveMuxTheme } from '../mux-theme.ts';
10
+
11
+ interface NimPaneLayout {
12
+ readonly rightCols: number;
13
+ readonly paneRows: number;
14
+ }
15
+
16
+ interface NimPaneRenderInput {
17
+ readonly layout: NimPaneLayout;
18
+ readonly viewModel: NimPaneViewModel;
19
+ }
20
+
21
+ interface NimPaneRenderResult {
22
+ readonly rows: readonly string[];
23
+ }
24
+
25
+ const HEADER = 'nim';
26
+ const COMPOSER_PROMPT = 'nim> ';
27
+ const USER_TRANSCRIPT_PREFIX = 'you> ';
28
+ const ASSISTANT_TRANSCRIPT_PREFIX = 'nim> ';
29
+ const uiKit = new UiKit();
30
+
31
+ export interface NimPaneViewModel {
32
+ readonly sessionId: string | null;
33
+ readonly status: 'thinking' | 'tool-calling' | 'responding' | 'idle';
34
+ readonly uiMode: 'debug' | 'user';
35
+ readonly composerText: string;
36
+ readonly queuedCount: number;
37
+ readonly transcriptLines: readonly string[];
38
+ readonly assistantDraftText: string;
39
+ }
40
+
41
+ export class NimPane {
42
+ render(input: NimPaneRenderInput): NimPaneRenderResult {
43
+ const viewModel = input.viewModel;
44
+ const safeRows = Math.max(0, input.layout.paneRows);
45
+ const safeCols = Math.max(1, input.layout.rightCols);
46
+ if (safeRows === 0) {
47
+ return { rows: [] };
48
+ }
49
+ const theme = getActiveMuxTheme();
50
+ const railTheme = theme.workspaceRail;
51
+ const conversationTheme = theme.conversationRail;
52
+ const surface = new SurfaceBuffer(safeCols, safeRows, DEFAULT_UI_STYLE);
53
+ const backgroundStyle = withStyle(railTheme.normalStyle, {
54
+ bg: resolveDefaultBackgroundColor(theme),
55
+ });
56
+ for (let row = 0; row < safeRows; row += 1) {
57
+ surface.fillRow(row, backgroundStyle);
58
+ }
59
+ const topBandFill = withStyle(conversationTheme.headerStyle, {
60
+ bg: resolveTopBandBackgroundColor(theme),
61
+ dim: false,
62
+ });
63
+ const topBandText = withStyle(railTheme.headerStyle, {
64
+ bg: topBandFill.bg,
65
+ bold: true,
66
+ });
67
+ const bodyText = withStyle(railTheme.normalStyle, {
68
+ bg: backgroundStyle.bg,
69
+ });
70
+ const mutedText = withStyle(railTheme.mutedStyle, {
71
+ bg: backgroundStyle.bg,
72
+ dim: true,
73
+ });
74
+ const actionText = withStyle(railTheme.actionStyle, {
75
+ bg: backgroundStyle.bg,
76
+ bold: true,
77
+ });
78
+ const statusBadge = statusBadgeStyle(viewModel.status, railTheme.statusColors);
79
+
80
+ paintRow(surface, 0, ` ${HEADER}`, topBandText, topBandFill);
81
+ drawStatusChip(surface, 0, safeCols, viewModel.status, statusBadge);
82
+ if (safeRows > 1) {
83
+ const sessionLabel =
84
+ viewModel.sessionId === null ? 'no-session' : viewModel.sessionId.slice(0, 8);
85
+ paintRow(
86
+ surface,
87
+ 1,
88
+ ` session:${sessionLabel} mode:${viewModel.uiMode} queued:${String(viewModel.queuedCount)}`,
89
+ withStyle(railTheme.metaStyle, {
90
+ bg: topBandFill.bg,
91
+ }),
92
+ topBandFill,
93
+ );
94
+ }
95
+ if (safeRows > 2) {
96
+ paintRow(
97
+ surface,
98
+ 2,
99
+ ' enter=send/steer tab=queue esc=abort /mode debug|user',
100
+ mutedText,
101
+ topBandFill,
102
+ );
103
+ }
104
+ if (safeRows > 3) {
105
+ paintSectionDivider(surface, 3, 'transcript', mutedText, backgroundStyle);
106
+ }
107
+
108
+ const composerDividerRow = Math.max(0, safeRows - 2);
109
+ const composerRow = Math.max(0, safeRows - 1);
110
+ const composerFill = withStyle(railTheme.activeRowStyle, {
111
+ bg: resolveComposerBackgroundColor(theme),
112
+ });
113
+ paintSectionDivider(surface, composerDividerRow, 'composer', mutedText, composerFill);
114
+ surface.fillRow(composerRow, composerFill);
115
+ const promptWidth = measureDisplayWidth(COMPOSER_PROMPT);
116
+ surface.drawText(0, composerRow, COMPOSER_PROMPT, actionText);
117
+ surface.drawText(promptWidth, composerRow, viewModel.composerText, bodyText);
118
+
119
+ const transcriptStartRow = Math.min(4, safeRows - 1);
120
+ const transcriptEndRow = Math.max(transcriptStartRow - 1, composerDividerRow - 1);
121
+ const transcriptCapacity = Math.max(0, transcriptEndRow - transcriptStartRow + 1);
122
+ const assistantDraftRow =
123
+ viewModel.assistantDraftText.length > 0 ? [`nim> ${viewModel.assistantDraftText}`] : [];
124
+ const transcriptRows = [...viewModel.transcriptLines, ...assistantDraftRow];
125
+ const visibleRows =
126
+ transcriptCapacity === 0
127
+ ? []
128
+ : transcriptRows.slice(Math.max(0, transcriptRows.length - transcriptCapacity));
129
+ for (let index = 0; index < visibleRows.length; index += 1) {
130
+ const row = visibleRows[index];
131
+ if (row === undefined) {
132
+ continue;
133
+ }
134
+ const rowIndex = transcriptStartRow + index;
135
+ const formatted = formatTranscriptLine(row);
136
+ surface.fillRow(rowIndex, backgroundStyle);
137
+ surface.drawText(1, rowIndex, formatted.symbol, formatted.symbolStyle);
138
+ uiKit.paintRow(
139
+ surface,
140
+ rowIndex,
141
+ ` ${formatted.text}`,
142
+ formatted.textStyle,
143
+ backgroundStyle,
144
+ );
145
+ }
146
+
147
+ return {
148
+ rows: surface.renderAnsiRows(),
149
+ };
150
+ }
151
+ }
152
+
153
+ function formatTranscriptLine(line: string): {
154
+ readonly symbol: string;
155
+ readonly symbolStyle: UiStyle;
156
+ readonly text: string;
157
+ readonly textStyle: UiStyle;
158
+ } {
159
+ const theme = getActiveMuxTheme();
160
+ const railTheme = theme.workspaceRail;
161
+ const bodyText = withStyle(railTheme.normalStyle, {
162
+ bg: resolveDefaultBackgroundColor(theme),
163
+ });
164
+ const mutedText = withStyle(railTheme.mutedStyle, {
165
+ bg: resolveDefaultBackgroundColor(theme),
166
+ dim: true,
167
+ });
168
+ const accentText = withStyle(railTheme.actionStyle, {
169
+ bg: resolveDefaultBackgroundColor(theme),
170
+ bold: false,
171
+ });
172
+ if (line.startsWith(USER_TRANSCRIPT_PREFIX)) {
173
+ return {
174
+ symbol: '›',
175
+ symbolStyle: accentText,
176
+ text: line,
177
+ textStyle: accentText,
178
+ };
179
+ }
180
+ if (line.startsWith(ASSISTANT_TRANSCRIPT_PREFIX)) {
181
+ return {
182
+ symbol: '•',
183
+ symbolStyle: bodyText,
184
+ text: line,
185
+ textStyle: bodyText,
186
+ };
187
+ }
188
+ if (line.startsWith('[error]')) {
189
+ return {
190
+ symbol: '!',
191
+ symbolStyle: withStyle(railTheme.actionStyle, {
192
+ fg: railTheme.statusColors.exited,
193
+ bg: resolveDefaultBackgroundColor(theme),
194
+ bold: true,
195
+ }),
196
+ text: line,
197
+ textStyle: withStyle(railTheme.metaStyle, {
198
+ fg: railTheme.statusColors.exited,
199
+ bg: resolveDefaultBackgroundColor(theme),
200
+ }),
201
+ };
202
+ }
203
+ if (line.startsWith('[tool:')) {
204
+ return {
205
+ symbol: '↳',
206
+ symbolStyle: withStyle(railTheme.metaStyle, {
207
+ fg: railTheme.statusColors.starting,
208
+ bg: resolveDefaultBackgroundColor(theme),
209
+ }),
210
+ text: line,
211
+ textStyle: mutedText,
212
+ };
213
+ }
214
+ return {
215
+ symbol: '•',
216
+ symbolStyle: mutedText,
217
+ text: line,
218
+ textStyle: mutedText,
219
+ };
220
+ }
221
+
222
+ function paintRow(
223
+ surface: SurfaceBuffer,
224
+ row: number,
225
+ text: string,
226
+ textStyle: UiStyle,
227
+ fillStyle: UiStyle,
228
+ ): void {
229
+ surface.fillRow(row, fillStyle);
230
+ uiKit.paintRow(surface, row, text, textStyle, fillStyle);
231
+ }
232
+
233
+ function paintSectionDivider(
234
+ surface: SurfaceBuffer,
235
+ row: number,
236
+ label: string,
237
+ textStyle: UiStyle,
238
+ fillStyle: UiStyle,
239
+ ): void {
240
+ const divider = ` ${'-'.repeat(Math.max(0, surface.cols - label.length - 3))} ${label}`;
241
+ paintRow(surface, row, divider, textStyle, fillStyle);
242
+ }
243
+
244
+ function withStyle(base: UiStyle, overrides: Partial<UiStyle>): UiStyle {
245
+ return {
246
+ fg: overrides.fg ?? base.fg,
247
+ bg: overrides.bg ?? base.bg,
248
+ bold: overrides.bold ?? base.bold,
249
+ ...(resolveStyleFlag(base.dim, overrides.dim) ? { dim: true } : {}),
250
+ ...(resolveStyleFlag(base.italic, overrides.italic) ? { italic: true } : {}),
251
+ ...(resolveStyleFlag(base.underline, overrides.underline) ? { underline: true } : {}),
252
+ ...(resolveStyleFlag(base.inverse, overrides.inverse) ? { inverse: true } : {}),
253
+ };
254
+ }
255
+
256
+ function resolveStyleFlag(base: boolean | undefined, override: boolean | undefined): boolean {
257
+ if (override === undefined) {
258
+ return base === true;
259
+ }
260
+ return override;
261
+ }
262
+
263
+ function resolveDefaultBackgroundColor(theme: ReturnType<typeof getActiveMuxTheme>): UiColor {
264
+ const bg = theme.conversationRail.normalRowStyle.bg;
265
+ return bg.kind === 'default' ? theme.workspaceRail.normalStyle.bg : bg;
266
+ }
267
+
268
+ function resolveTopBandBackgroundColor(theme: ReturnType<typeof getActiveMuxTheme>): UiColor {
269
+ const bg = theme.conversationRail.headerStyle.bg;
270
+ return bg.kind === 'default' ? resolveDefaultBackgroundColor(theme) : bg;
271
+ }
272
+
273
+ function resolveComposerBackgroundColor(theme: ReturnType<typeof getActiveMuxTheme>): UiColor {
274
+ const bg = theme.workspaceRail.activeRowStyle.bg;
275
+ return bg.kind === 'default' ? resolveTopBandBackgroundColor(theme) : bg;
276
+ }
277
+
278
+ function statusBadgeStyle(
279
+ status: NimPaneViewModel['status'],
280
+ colors: {
281
+ readonly working: UiColor;
282
+ readonly exited: UiColor;
283
+ readonly needsAction: UiColor;
284
+ readonly starting: UiColor;
285
+ readonly idle: UiColor;
286
+ },
287
+ ): UiStyle {
288
+ const color =
289
+ status === 'thinking'
290
+ ? colors.starting
291
+ : status === 'tool-calling'
292
+ ? colors.needsAction
293
+ : status === 'responding'
294
+ ? colors.working
295
+ : colors.idle;
296
+ return {
297
+ fg: color,
298
+ bg: { kind: 'default' },
299
+ bold: true,
300
+ inverse: true,
301
+ };
302
+ }
303
+
304
+ function drawStatusChip(
305
+ surface: SurfaceBuffer,
306
+ row: number,
307
+ cols: number,
308
+ status: NimPaneViewModel['status'],
309
+ style: UiStyle,
310
+ ): void {
311
+ const label = ` ${status} `;
312
+ const width = measureDisplayWidth(label);
313
+ const col = Math.max(0, cols - width - 1);
314
+ surface.drawText(col, row, label, style);
315
+ }
@@ -1,115 +0,0 @@
1
- import type { TaskPaneAction } from '../harness-core-ui.ts';
2
-
3
- interface TaskRecordActionState {
4
- readonly taskId: string;
5
- readonly status: string;
6
- }
7
-
8
- interface RunTaskPaneActionOptions {
9
- action: TaskPaneAction;
10
- openTaskCreatePrompt: () => void;
11
- openRepositoryPromptForCreate: () => void;
12
- selectedRepositoryId: string | null;
13
- repositoryExists: (repositoryId: string) => boolean;
14
- setTaskPaneNotice: (notice: string | null) => void;
15
- markDirty: () => void;
16
- setTaskPaneSelectionFocus: (focus: 'task' | 'repository') => void;
17
- openRepositoryPromptForEdit: (repositoryId: string) => void;
18
- queueArchiveRepository: (repositoryId: string) => void;
19
- selectedTask: TaskRecordActionState | null;
20
- openTaskEditPrompt: (taskId: string) => void;
21
- queueDeleteTask: (taskId: string) => void;
22
- queueTaskReady: (taskId: string) => void;
23
- queueTaskDraft: (taskId: string) => void;
24
- queueTaskComplete: (taskId: string) => void;
25
- orderedTaskRecords: () => readonly TaskRecordActionState[];
26
- queueTaskReorderByIds: (orderedTaskIds: readonly string[], label: string) => void;
27
- }
28
-
29
- export function runTaskPaneAction(options: RunTaskPaneActionOptions): void {
30
- if (options.action === 'task.create') {
31
- options.openTaskCreatePrompt();
32
- return;
33
- }
34
- if (options.action === 'repository.create') {
35
- options.setTaskPaneNotice(null);
36
- options.openRepositoryPromptForCreate();
37
- return;
38
- }
39
- if (options.action === 'repository.edit') {
40
- const selectedRepositoryId = options.selectedRepositoryId;
41
- if (selectedRepositoryId === null || !options.repositoryExists(selectedRepositoryId)) {
42
- options.setTaskPaneNotice('select a repository first');
43
- options.markDirty();
44
- return;
45
- }
46
- options.setTaskPaneSelectionFocus('repository');
47
- options.setTaskPaneNotice(null);
48
- options.openRepositoryPromptForEdit(selectedRepositoryId);
49
- return;
50
- }
51
- if (options.action === 'repository.archive') {
52
- const selectedRepositoryId = options.selectedRepositoryId;
53
- if (selectedRepositoryId === null || !options.repositoryExists(selectedRepositoryId)) {
54
- options.setTaskPaneNotice('select a repository first');
55
- options.markDirty();
56
- return;
57
- }
58
- options.setTaskPaneSelectionFocus('repository');
59
- options.queueArchiveRepository(selectedRepositoryId);
60
- return;
61
- }
62
- const selected = options.selectedTask;
63
- if (selected === null) {
64
- options.setTaskPaneNotice('select a task first');
65
- options.markDirty();
66
- return;
67
- }
68
- if (options.action === 'task.edit') {
69
- options.setTaskPaneSelectionFocus('task');
70
- options.openTaskEditPrompt(selected.taskId);
71
- return;
72
- }
73
- if (options.action === 'task.delete') {
74
- options.setTaskPaneSelectionFocus('task');
75
- options.queueDeleteTask(selected.taskId);
76
- return;
77
- }
78
- if (options.action === 'task.ready') {
79
- options.setTaskPaneSelectionFocus('task');
80
- options.queueTaskReady(selected.taskId);
81
- return;
82
- }
83
- if (options.action === 'task.draft') {
84
- options.setTaskPaneSelectionFocus('task');
85
- options.queueTaskDraft(selected.taskId);
86
- return;
87
- }
88
- if (options.action === 'task.complete') {
89
- options.setTaskPaneSelectionFocus('task');
90
- options.queueTaskComplete(selected.taskId);
91
- return;
92
- }
93
- if (options.action === 'task.reorder-up' || options.action === 'task.reorder-down') {
94
- const activeTasks = options.orderedTaskRecords().filter((task) => task.status !== 'completed');
95
- const selectedIndex = activeTasks.findIndex((task) => task.taskId === selected.taskId);
96
- if (selectedIndex < 0) {
97
- options.setTaskPaneNotice('cannot reorder completed tasks');
98
- options.markDirty();
99
- return;
100
- }
101
- const swapIndex = options.action === 'task.reorder-up' ? selectedIndex - 1 : selectedIndex + 1;
102
- if (swapIndex < 0 || swapIndex >= activeTasks.length) {
103
- return;
104
- }
105
- const reordered = [...activeTasks];
106
- const currentTask = reordered[selectedIndex]!;
107
- reordered[selectedIndex] = reordered[swapIndex]!;
108
- reordered[swapIndex] = currentTask;
109
- options.setTaskPaneSelectionFocus('task');
110
- options.queueTaskReorderByIds(
111
- reordered.map((task) => task.taskId),
112
- options.action === 'task.reorder-up' ? 'tasks-reorder-up' : 'tasks-reorder-down',
113
- );
114
- }
115
- }