@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
@@ -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
- }