@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,11 +1,19 @@
1
1
  import type { WorkspaceModel } from '../domain/workspace.ts';
2
2
  import type { PaneSelection, PaneSelectionDrag } from '../mux/live-mux/selection.ts';
3
3
 
4
- interface RuntimeRenderStateOptions<TConversation, TFrame> {
4
+ interface RuntimeRenderStateDirectoryLookup {
5
+ hasDirectory(directoryId: string): boolean;
6
+ }
7
+
8
+ interface RuntimeRenderStateConversationLookup<TConversation> {
9
+ readonly activeConversationId: string | null;
10
+ getActiveConversation(): TConversation | null;
11
+ }
12
+
13
+ export interface RuntimeRenderStateOptions<TConversation, TFrame> {
5
14
  readonly workspace: WorkspaceModel;
6
- readonly hasDirectory: (directoryId: string) => boolean;
7
- readonly activeConversationId: () => string | null;
8
- readonly activeConversation: () => TConversation | null;
15
+ readonly directories: RuntimeRenderStateDirectoryLookup;
16
+ readonly conversations: RuntimeRenderStateConversationLookup<TConversation>;
9
17
  readonly snapshotFrame: (conversation: TConversation) => TFrame;
10
18
  readonly selectionVisibleRows: (
11
19
  frame: TFrame,
@@ -13,60 +21,65 @@ interface RuntimeRenderStateOptions<TConversation, TFrame> {
13
21
  ) => readonly number[];
14
22
  }
15
23
 
16
- interface RuntimeRenderStateResult<TConversation, TFrame> {
24
+ export interface RuntimeRenderStateResult<TConversation, TFrame> {
17
25
  readonly projectPaneActive: boolean;
18
26
  readonly homePaneActive: boolean;
27
+ readonly nimPaneActive: boolean;
19
28
  readonly activeConversation: TConversation | null;
20
29
  readonly rightFrame: TFrame | null;
21
30
  readonly renderSelection: PaneSelection | null;
22
31
  readonly selectionRows: readonly number[];
23
32
  }
24
33
 
25
- export class RuntimeRenderState<TConversation, TFrame> {
26
- constructor(private readonly options: RuntimeRenderStateOptions<TConversation, TFrame>) {}
27
-
28
- prepareRenderState(
29
- selection: PaneSelection | null,
30
- selectionDrag: PaneSelectionDrag | null,
31
- ): RuntimeRenderStateResult<TConversation, TFrame> | null {
32
- const workspace = this.options.workspace;
33
- const projectPaneActive =
34
- workspace.mainPaneMode === 'project' &&
35
- workspace.activeDirectoryId !== null &&
36
- this.options.hasDirectory(workspace.activeDirectoryId);
37
- const homePaneActive = workspace.mainPaneMode === 'home';
38
- if (!projectPaneActive && !homePaneActive && this.options.activeConversationId() === null) {
39
- return null;
40
- }
34
+ export function prepareRuntimeRenderState<TConversation, TFrame>(
35
+ options: RuntimeRenderStateOptions<TConversation, TFrame>,
36
+ selection: PaneSelection | null,
37
+ selectionDrag: PaneSelectionDrag | null,
38
+ ): RuntimeRenderStateResult<TConversation, TFrame> | null {
39
+ const workspace = options.workspace;
40
+ const projectPaneActive =
41
+ workspace.mainPaneMode === 'project' &&
42
+ workspace.activeDirectoryId !== null &&
43
+ options.directories.hasDirectory(workspace.activeDirectoryId);
44
+ const homePaneActive = workspace.mainPaneMode === 'home';
45
+ const nimPaneActive = workspace.mainPaneMode === 'nim';
46
+ if (
47
+ !projectPaneActive &&
48
+ !homePaneActive &&
49
+ !nimPaneActive &&
50
+ options.conversations.activeConversationId === null
51
+ ) {
52
+ return null;
53
+ }
41
54
 
42
- const activeConversation = this.options.activeConversation();
43
- if (!projectPaneActive && !homePaneActive && activeConversation === null) {
44
- return null;
45
- }
55
+ const activeConversation = options.conversations.getActiveConversation();
56
+ if (!projectPaneActive && !homePaneActive && !nimPaneActive && activeConversation === null) {
57
+ return null;
58
+ }
46
59
 
47
- const rightFrame =
48
- !projectPaneActive && !homePaneActive && activeConversation !== null
49
- ? this.options.snapshotFrame(activeConversation)
60
+ const rightFrame =
61
+ !projectPaneActive && !homePaneActive && !nimPaneActive && activeConversation !== null
62
+ ? options.snapshotFrame(activeConversation)
63
+ : null;
64
+ const renderSelection =
65
+ rightFrame !== null && selectionDrag !== null && selectionDrag.hasDragged
66
+ ? {
67
+ anchor: selectionDrag.anchor,
68
+ focus: selectionDrag.focus,
69
+ text: '',
70
+ }
71
+ : rightFrame !== null
72
+ ? selection
50
73
  : null;
51
- const renderSelection =
52
- rightFrame !== null && selectionDrag !== null && selectionDrag.hasDragged
53
- ? {
54
- anchor: selectionDrag.anchor,
55
- focus: selectionDrag.focus,
56
- text: '',
57
- }
58
- : rightFrame !== null
59
- ? selection
60
- : null;
61
- const selectionRows =
62
- rightFrame === null ? [] : this.options.selectionVisibleRows(rightFrame, renderSelection);
63
- return {
64
- projectPaneActive,
65
- homePaneActive,
66
- activeConversation,
67
- rightFrame,
68
- renderSelection,
69
- selectionRows,
70
- };
71
- }
74
+ const selectionRows =
75
+ rightFrame === null ? [] : options.selectionVisibleRows(rightFrame, renderSelection);
76
+ return {
77
+ projectPaneActive,
78
+ homePaneActive,
79
+ nimPaneActive,
80
+ activeConversation,
81
+ rightFrame,
82
+ renderSelection,
83
+ selectionRows,
84
+ };
72
85
  }
@@ -24,7 +24,7 @@ interface RuntimeRepositoryActionService<TRepository extends RepositoryRecordSha
24
24
  archiveRepository(repositoryId: string): Promise<unknown>;
25
25
  }
26
26
 
27
- interface RuntimeRepositoryActionsOptions<TRepository extends RepositoryRecordShape> {
27
+ export interface RuntimeRepositoryActionsOptions<TRepository extends RepositoryRecordShape> {
28
28
  readonly workspace: WorkspaceModel;
29
29
  readonly repositories: Map<string, TRepository>;
30
30
  readonly controlPlaneService: RuntimeRepositoryActionService<TRepository>;
@@ -49,51 +49,85 @@ function repositoryHomePriority(repository: RepositoryRecordShape): number | nul
49
49
  return raw;
50
50
  }
51
51
 
52
- export class RuntimeRepositoryActions<TRepository extends RepositoryRecordShape> {
53
- constructor(private readonly options: RuntimeRepositoryActionsOptions<TRepository>) {}
52
+ export interface RuntimeRepositoryActions {
53
+ openRepositoryPromptForCreate(): void;
54
+ openRepositoryPromptForEdit(repositoryId: string): void;
55
+ queueRepositoryPriorityOrder(orderedRepositoryIds: readonly string[], label: string): void;
56
+ reorderRepositoryByDrop(
57
+ draggedRepositoryId: string,
58
+ targetRepositoryId: string,
59
+ orderedRepositoryIds: readonly string[],
60
+ ): void;
61
+ upsertRepositoryByRemoteUrl(remoteUrl: string, existingRepositoryId?: string): Promise<void>;
62
+ archiveRepositoryById(repositoryId: string): Promise<void>;
63
+ }
64
+
65
+ export function createRuntimeRepositoryActions<TRepository extends RepositoryRecordShape>(
66
+ options: RuntimeRepositoryActionsOptions<TRepository>,
67
+ ): RuntimeRepositoryActions {
68
+ const reorderIdsByMove = (
69
+ orderedIds: readonly string[],
70
+ movedId: string,
71
+ targetId: string,
72
+ ): readonly string[] | null => {
73
+ const fromIndex = orderedIds.indexOf(movedId);
74
+ const targetIndex = orderedIds.indexOf(targetId);
75
+ if (fromIndex < 0 || targetIndex < 0 || fromIndex === targetIndex) {
76
+ return null;
77
+ }
78
+ const reordered = [...orderedIds];
79
+ const moved = reordered.splice(fromIndex, 1)[0]!;
80
+ reordered.splice(targetIndex, 0, moved);
81
+ return reordered;
82
+ };
54
83
 
55
- openRepositoryPromptForCreate(): void {
56
- this.options.workspace.newThreadPrompt = null;
57
- this.options.workspace.addDirectoryPrompt = null;
58
- if (this.options.workspace.conversationTitleEdit !== null) {
59
- this.options.stopConversationTitleEdit();
84
+ const openRepositoryPromptForCreate = (): void => {
85
+ options.workspace.newThreadPrompt = null;
86
+ options.workspace.addDirectoryPrompt = null;
87
+ options.workspace.apiKeyPrompt = null;
88
+ if (options.workspace.conversationTitleEdit !== null) {
89
+ options.stopConversationTitleEdit();
60
90
  }
61
- this.options.workspace.conversationTitleEditClickState = null;
62
- this.options.workspace.repositoryPrompt = {
91
+ options.workspace.conversationTitleEditClickState = null;
92
+ options.workspace.repositoryPrompt = {
63
93
  mode: 'add',
64
94
  repositoryId: null,
65
95
  value: '',
66
96
  error: null,
67
97
  };
68
- this.options.markDirty();
69
- }
98
+ options.markDirty();
99
+ };
70
100
 
71
- openRepositoryPromptForEdit(repositoryId: string): void {
72
- const repository = this.options.repositories.get(repositoryId);
101
+ const openRepositoryPromptForEdit = (repositoryId: string): void => {
102
+ const repository = options.repositories.get(repositoryId);
73
103
  if (repository === undefined) {
74
104
  return;
75
105
  }
76
- this.options.workspace.newThreadPrompt = null;
77
- this.options.workspace.addDirectoryPrompt = null;
78
- if (this.options.workspace.conversationTitleEdit !== null) {
79
- this.options.stopConversationTitleEdit();
106
+ options.workspace.newThreadPrompt = null;
107
+ options.workspace.addDirectoryPrompt = null;
108
+ options.workspace.apiKeyPrompt = null;
109
+ if (options.workspace.conversationTitleEdit !== null) {
110
+ options.stopConversationTitleEdit();
80
111
  }
81
- this.options.workspace.conversationTitleEditClickState = null;
82
- this.options.workspace.repositoryPrompt = {
112
+ options.workspace.conversationTitleEditClickState = null;
113
+ options.workspace.repositoryPrompt = {
83
114
  mode: 'edit',
84
115
  repositoryId,
85
116
  value: repository.remoteUrl,
86
117
  error: null,
87
118
  };
88
- this.options.workspace.taskPaneSelectionFocus = 'repository';
89
- this.options.markDirty();
90
- }
119
+ options.workspace.taskPaneSelectionFocus = 'repository';
120
+ options.markDirty();
121
+ };
91
122
 
92
- queueRepositoryPriorityOrder(orderedRepositoryIds: readonly string[], label: string): void {
123
+ const queueRepositoryPriorityOrder = (
124
+ orderedRepositoryIds: readonly string[],
125
+ label: string,
126
+ ): void => {
93
127
  const updates: Array<{ repositoryId: string; metadata: Record<string, unknown> }> = [];
94
128
  for (let index = 0; index < orderedRepositoryIds.length; index += 1) {
95
129
  const repositoryId = orderedRepositoryIds[index]!;
96
- const repository = this.options.repositories.get(repositoryId);
130
+ const repository = options.repositories.get(repositoryId);
97
131
  if (repository === undefined) {
98
132
  continue;
99
133
  }
@@ -111,25 +145,25 @@ export class RuntimeRepositoryActions<TRepository extends RepositoryRecordShape>
111
145
  if (updates.length === 0) {
112
146
  return;
113
147
  }
114
- this.options.queueControlPlaneOp(async () => {
148
+ options.queueControlPlaneOp(async () => {
115
149
  for (const update of updates) {
116
- const repository = await this.options.controlPlaneService.updateRepository({
150
+ const repository = await options.controlPlaneService.updateRepository({
117
151
  repositoryId: update.repositoryId,
118
152
  metadata: update.metadata,
119
153
  });
120
- this.options.repositories.set(repository.repositoryId, repository);
154
+ options.repositories.set(repository.repositoryId, repository);
121
155
  }
122
- this.options.syncTaskPaneRepositorySelection();
123
- this.options.markDirty();
156
+ options.syncTaskPaneRepositorySelection();
157
+ options.markDirty();
124
158
  }, label);
125
- }
159
+ };
126
160
 
127
- reorderRepositoryByDrop(
161
+ const reorderRepositoryByDrop = (
128
162
  draggedRepositoryId: string,
129
163
  targetRepositoryId: string,
130
164
  orderedRepositoryIds: readonly string[],
131
- ): void {
132
- const reordered = this.reorderIdsByMove(
165
+ ): void => {
166
+ const reordered = reorderIdsByMove(
133
167
  orderedRepositoryIds,
134
168
  draggedRepositoryId,
135
169
  targetRepositoryId,
@@ -137,22 +171,22 @@ export class RuntimeRepositoryActions<TRepository extends RepositoryRecordShape>
137
171
  if (reordered === null) {
138
172
  return;
139
173
  }
140
- this.queueRepositoryPriorityOrder(reordered, 'repositories-reorder-drag');
141
- }
174
+ queueRepositoryPriorityOrder(reordered, 'repositories-reorder-drag');
175
+ };
142
176
 
143
- async upsertRepositoryByRemoteUrl(
177
+ const upsertRepositoryByRemoteUrl = async (
144
178
  remoteUrl: string,
145
179
  existingRepositoryId?: string,
146
- ): Promise<void> {
147
- const normalizedRemoteUrl = this.options.normalizeGitHubRemoteUrl(remoteUrl);
180
+ ): Promise<void> => {
181
+ const normalizedRemoteUrl = options.normalizeGitHubRemoteUrl(remoteUrl);
148
182
  if (normalizedRemoteUrl === null) {
149
183
  throw new Error('github url required');
150
184
  }
151
- const repositoryName = this.options.repositoryNameFromGitHubRemoteUrl(normalizedRemoteUrl);
185
+ const repositoryName = options.repositoryNameFromGitHubRemoteUrl(normalizedRemoteUrl);
152
186
  const repository =
153
187
  existingRepositoryId === undefined
154
- ? await this.options.controlPlaneService.upsertRepository({
155
- repositoryId: this.options.createRepositoryId(),
188
+ ? await options.controlPlaneService.upsertRepository({
189
+ repositoryId: options.createRepositoryId(),
156
190
  name: repositoryName,
157
191
  remoteUrl: normalizedRemoteUrl,
158
192
  defaultBranch: 'main',
@@ -160,38 +194,31 @@ export class RuntimeRepositoryActions<TRepository extends RepositoryRecordShape>
160
194
  source: 'mux-manual',
161
195
  },
162
196
  })
163
- : await this.options.controlPlaneService.updateRepository({
197
+ : await options.controlPlaneService.updateRepository({
164
198
  repositoryId: existingRepositoryId,
165
199
  name: repositoryName,
166
200
  remoteUrl: normalizedRemoteUrl,
167
201
  });
168
- this.options.repositories.set(repository.repositoryId, repository);
169
- this.options.syncRepositoryAssociationsWithDirectorySnapshots();
170
- this.options.syncTaskPaneRepositorySelection();
171
- this.options.markDirty();
172
- }
202
+ options.repositories.set(repository.repositoryId, repository);
203
+ options.syncRepositoryAssociationsWithDirectorySnapshots();
204
+ options.syncTaskPaneRepositorySelection();
205
+ options.markDirty();
206
+ };
173
207
 
174
- async archiveRepositoryById(repositoryId: string): Promise<void> {
175
- await this.options.controlPlaneService.archiveRepository(repositoryId);
176
- this.options.repositories.delete(repositoryId);
177
- this.options.syncRepositoryAssociationsWithDirectorySnapshots();
178
- this.options.syncTaskPaneRepositorySelection();
179
- this.options.markDirty();
180
- }
208
+ const archiveRepositoryById = async (repositoryId: string): Promise<void> => {
209
+ await options.controlPlaneService.archiveRepository(repositoryId);
210
+ options.repositories.delete(repositoryId);
211
+ options.syncRepositoryAssociationsWithDirectorySnapshots();
212
+ options.syncTaskPaneRepositorySelection();
213
+ options.markDirty();
214
+ };
181
215
 
182
- private reorderIdsByMove(
183
- orderedIds: readonly string[],
184
- movedId: string,
185
- targetId: string,
186
- ): readonly string[] | null {
187
- const fromIndex = orderedIds.indexOf(movedId);
188
- const targetIndex = orderedIds.indexOf(targetId);
189
- if (fromIndex < 0 || targetIndex < 0 || fromIndex === targetIndex) {
190
- return null;
191
- }
192
- const reordered = [...orderedIds];
193
- const moved = reordered.splice(fromIndex, 1)[0]!;
194
- reordered.splice(targetIndex, 0, moved);
195
- return reordered;
196
- }
216
+ return {
217
+ openRepositoryPromptForCreate,
218
+ openRepositoryPromptForEdit,
219
+ queueRepositoryPriorityOrder,
220
+ reorderRepositoryByDrop,
221
+ upsertRepositoryByRemoteUrl,
222
+ archiveRepositoryById,
223
+ };
197
224
  }
@@ -1,7 +1,7 @@
1
1
  import type { WorkspaceModel } from '../domain/workspace.ts';
2
- import type { TaskManager } from '../domain/tasks.ts';
3
2
  import type { ProjectPaneSnapshot } from '../mux/harness-core-ui.ts';
4
3
  import type { TaskComposerBuffer } from '../mux/task-composer.ts';
4
+ import type { NimPaneViewModel } from '../ui/panes/nim.ts';
5
5
  import type {
6
6
  TaskFocusedPaneRepositoryRecord,
7
7
  TaskFocusedPaneTaskRecord,
@@ -9,7 +9,7 @@ import type {
9
9
  } from '../mux/task-focused-pane.ts';
10
10
  import type { TerminalSnapshotFrameCore } from '../terminal/snapshot-oracle.ts';
11
11
 
12
- interface RuntimeRightPaneLayout {
12
+ export interface RuntimeRightPaneLayout {
13
13
  readonly rightCols: number;
14
14
  readonly paneRows: number;
15
15
  }
@@ -25,6 +25,7 @@ interface HomePaneRenderInput<
25
25
  readonly layout: RuntimeRightPaneLayout;
26
26
  readonly repositories: ReadonlyMap<string, TRepositoryRecord>;
27
27
  readonly tasks: ReadonlyMap<string, TTaskRecord>;
28
+ readonly showTaskPlanningUi?: boolean;
28
29
  readonly selectedRepositoryId: string | null;
29
30
  readonly repositoryDropdownOpen: boolean;
30
31
  readonly editorTarget: WorkspaceModel['taskEditorTarget'];
@@ -52,81 +53,107 @@ interface ProjectPaneLike {
52
53
  };
53
54
  }
54
55
 
55
- interface RuntimeRightPaneRenderInput {
56
+ interface NimPaneLike {
57
+ render(input: { layout: RuntimeRightPaneLayout; viewModel: NimPaneViewModel }): {
58
+ readonly rows: readonly string[];
59
+ };
60
+ }
61
+
62
+ export interface RuntimeRightPaneRenderInput<
63
+ TRepositoryRecord extends TaskFocusedPaneRepositoryRecord,
64
+ TTaskRecord extends TaskFocusedPaneTaskRecord,
65
+ > {
56
66
  readonly layout: RuntimeRightPaneLayout;
57
67
  readonly rightFrame: TerminalSnapshotFrameCore | null;
58
68
  readonly homePaneActive: boolean;
69
+ readonly nimPaneActive: boolean;
59
70
  readonly projectPaneActive: boolean;
60
71
  readonly activeDirectoryId: string | null;
72
+ readonly snapshot: RuntimeRightPaneRenderSnapshot<TRepositoryRecord, TTaskRecord>;
61
73
  }
62
74
 
63
- interface RuntimeRightPaneRenderOptions<
75
+ export interface RuntimeRightPaneRenderSnapshot<
64
76
  TRepositoryRecord extends TaskFocusedPaneRepositoryRecord,
65
77
  TTaskRecord extends TaskFocusedPaneTaskRecord,
66
78
  > {
67
- readonly workspace: WorkspaceModel;
68
79
  readonly repositories: ReadonlyMap<string, TRepositoryRecord>;
69
- readonly taskManager: TaskManager<TTaskRecord, TaskComposerBuffer, NodeJS.Timeout>;
80
+ readonly tasks: ReadonlyMap<string, TTaskRecord>;
81
+ readonly taskComposers: ReadonlyMap<string, TaskComposerBuffer>;
82
+ }
83
+
84
+ export interface RuntimeRightPaneRenderOptions<
85
+ TRepositoryRecord extends TaskFocusedPaneRepositoryRecord,
86
+ TTaskRecord extends TaskFocusedPaneTaskRecord,
87
+ > {
88
+ readonly workspace: WorkspaceModel;
89
+ readonly showTasks?: boolean;
70
90
  readonly conversationPane: ConversationPaneLike;
71
91
  readonly homePane: HomePaneLike<TRepositoryRecord, TTaskRecord>;
72
92
  readonly projectPane: ProjectPaneLike;
93
+ readonly nimPane: NimPaneLike;
94
+ readonly getNimViewModel: () => NimPaneViewModel;
73
95
  readonly refreshProjectPaneSnapshot: (directoryId: string) => ProjectPaneSnapshot | null;
74
96
  readonly emptyTaskPaneView: () => TaskFocusedPaneView;
75
97
  }
76
98
 
77
- export class RuntimeRightPaneRender<
99
+ export function renderRuntimeRightPaneRows<
78
100
  TRepositoryRecord extends TaskFocusedPaneRepositoryRecord,
79
101
  TTaskRecord extends TaskFocusedPaneTaskRecord,
80
- > {
81
- constructor(
82
- private readonly options: RuntimeRightPaneRenderOptions<TRepositoryRecord, TTaskRecord>,
83
- ) {}
102
+ >(
103
+ options: RuntimeRightPaneRenderOptions<TRepositoryRecord, TTaskRecord>,
104
+ input: RuntimeRightPaneRenderInput<TRepositoryRecord, TTaskRecord>,
105
+ ): readonly string[] {
106
+ const workspace = options.workspace;
107
+ workspace.latestTaskPaneView = options.emptyTaskPaneView();
84
108
 
85
- renderRightRows(input: RuntimeRightPaneRenderInput): readonly string[] {
86
- const workspace = this.options.workspace;
87
- workspace.latestTaskPaneView = this.options.emptyTaskPaneView();
109
+ if (input.rightFrame !== null) {
110
+ return options.conversationPane.render(input.rightFrame, input.layout);
111
+ }
88
112
 
89
- if (input.rightFrame !== null) {
90
- return this.options.conversationPane.render(input.rightFrame, input.layout);
91
- }
113
+ if (input.homePaneActive) {
114
+ const view = options.homePane.render({
115
+ layout: input.layout,
116
+ repositories: input.snapshot.repositories,
117
+ tasks: input.snapshot.tasks,
118
+ showTaskPlanningUi:
119
+ (options.showTasks ?? true) && workspace.leftNavSelection.kind === 'tasks',
120
+ selectedRepositoryId: workspace.taskPaneSelectedRepositoryId,
121
+ repositoryDropdownOpen: workspace.taskRepositoryDropdownOpen,
122
+ editorTarget: workspace.taskEditorTarget,
123
+ draftBuffer: workspace.taskDraftComposer,
124
+ taskBufferById: input.snapshot.taskComposers,
125
+ notice: workspace.taskPaneNotice,
126
+ scrollTop: workspace.taskPaneScrollTop,
127
+ });
128
+ workspace.taskPaneSelectedRepositoryId = view.selectedRepositoryId;
129
+ workspace.taskPaneScrollTop = view.top;
130
+ workspace.latestTaskPaneView = view;
131
+ return view.rows;
132
+ }
92
133
 
93
- if (input.homePaneActive) {
94
- const view = this.options.homePane.render({
95
- layout: input.layout,
96
- repositories: this.options.repositories,
97
- tasks: this.options.taskManager.readonlyTasks(),
98
- selectedRepositoryId: workspace.taskPaneSelectedRepositoryId,
99
- repositoryDropdownOpen: workspace.taskRepositoryDropdownOpen,
100
- editorTarget: workspace.taskEditorTarget,
101
- draftBuffer: workspace.taskDraftComposer,
102
- taskBufferById: this.options.taskManager.readonlyTaskComposers(),
103
- notice: workspace.taskPaneNotice,
104
- scrollTop: workspace.taskPaneScrollTop,
105
- });
106
- workspace.taskPaneSelectedRepositoryId = view.selectedRepositoryId;
107
- workspace.taskPaneScrollTop = view.top;
108
- workspace.latestTaskPaneView = view;
109
- return view.rows;
110
- }
134
+ if (input.nimPaneActive) {
135
+ const view = options.nimPane.render({
136
+ layout: input.layout,
137
+ viewModel: options.getNimViewModel(),
138
+ });
139
+ return view.rows;
140
+ }
111
141
 
112
- if (input.projectPaneActive && input.activeDirectoryId !== null) {
113
- const needsSnapshotRefresh =
114
- workspace.projectPaneSnapshot === null ||
115
- workspace.projectPaneSnapshot.directoryId !== input.activeDirectoryId;
116
- if (needsSnapshotRefresh) {
117
- workspace.projectPaneSnapshot = this.options.refreshProjectPaneSnapshot(
118
- input.activeDirectoryId,
119
- );
120
- }
121
- const view = this.options.projectPane.render({
122
- layout: input.layout,
123
- snapshot: workspace.projectPaneSnapshot,
124
- scrollTop: workspace.projectPaneScrollTop,
125
- });
126
- workspace.projectPaneScrollTop = view.scrollTop;
127
- return view.rows;
142
+ if (input.projectPaneActive && input.activeDirectoryId !== null) {
143
+ const needsSnapshotRefresh =
144
+ workspace.projectPaneSnapshot === null ||
145
+ workspace.projectPaneSnapshot.directoryId !== input.activeDirectoryId;
146
+ if (needsSnapshotRefresh) {
147
+ workspace.projectPaneSnapshot = options.refreshProjectPaneSnapshot(input.activeDirectoryId);
128
148
  }
129
-
130
- return Array.from({ length: input.layout.paneRows }, () => ' '.repeat(input.layout.rightCols));
149
+ const view = options.projectPane.render({
150
+ layout: input.layout,
151
+ snapshot: workspace.projectPaneSnapshot,
152
+ scrollTop: workspace.projectPaneScrollTop,
153
+ });
154
+ workspace.projectPaneScrollTop = view.scrollTop;
155
+ return view.rows;
131
156
  }
157
+
158
+ return Array.from({ length: input.layout.paneRows }, () => ' '.repeat(input.layout.rightCols));
132
159
  }
@@ -1,4 +1,4 @@
1
- interface RuntimeShutdownServiceOptions {
1
+ export interface RuntimeShutdownServiceOptions {
2
2
  readonly screen: {
3
3
  clearDirty: () => void;
4
4
  };
@@ -11,12 +11,15 @@ interface RuntimeShutdownServiceOptions {
11
11
  readonly clearResizeTimer: () => void;
12
12
  readonly clearPtyResizeTimer: () => void;
13
13
  readonly clearHomePaneBackgroundTimer: () => void;
14
+ readonly clearStorageLifecycleTimer: () => void;
15
+ readonly clearProjectPaneGitHubReviewRefreshTimer: () => void;
14
16
  readonly persistMuxUiStateNow: () => void;
15
17
  readonly clearConversationTitleEditTimer: () => void;
16
18
  readonly flushTaskComposerPersist: () => void;
17
19
  readonly clearRenderScheduled: () => void;
18
20
  readonly detachProcessListeners: () => void;
19
21
  readonly removeEnvelopeListener: () => void;
22
+ readonly stopWorkspaceObservedEvents: () => void;
20
23
  readonly unsubscribeTaskPlanningEvents: () => Promise<void>;
21
24
  readonly closeKeyEventSubscription: () => Promise<void>;
22
25
  readonly clearRuntimeFatalExitTimer: () => void;
@@ -41,39 +44,39 @@ interface RuntimeShutdownServiceOptions {
41
44
  readonly shutdownPerfCore: () => void;
42
45
  }
43
46
 
44
- export class RuntimeShutdownService {
45
- constructor(private readonly options: RuntimeShutdownServiceOptions) {}
46
-
47
- async finalize(): Promise<void> {
48
- this.options.screen.clearDirty();
49
- this.options.outputLoadSampler.stop();
50
- this.options.startupBackgroundProbeService.stop();
51
- this.options.clearResizeTimer();
52
- this.options.clearPtyResizeTimer();
53
- this.options.clearHomePaneBackgroundTimer();
54
- this.options.persistMuxUiStateNow();
55
- this.options.clearConversationTitleEditTimer();
56
- this.options.flushTaskComposerPersist();
57
- this.options.clearRenderScheduled();
58
- this.options.detachProcessListeners();
59
- this.options.removeEnvelopeListener();
60
- await this.options.unsubscribeTaskPlanningEvents();
61
- await this.options.closeKeyEventSubscription();
62
- this.options.clearRuntimeFatalExitTimer();
63
-
64
- try {
65
- await this.options.waitForControlPlaneDrain();
66
- await this.options.controlPlaneClient.close();
67
- } catch {
68
- // Best-effort shutdown only.
69
- }
70
-
71
- this.options.eventPersistence.flush('shutdown');
72
- const recordingCloseError = await this.options.recordingService.closeWriter();
73
- this.options.store.close();
74
- this.options.restoreTerminalState();
75
- await this.options.recordingService.finalizeAfterShutdown(recordingCloseError);
76
- this.options.startupShutdownService.finalize();
77
- this.options.shutdownPerfCore();
47
+ export async function finalizeRuntimeShutdown(
48
+ options: RuntimeShutdownServiceOptions,
49
+ ): Promise<void> {
50
+ options.screen.clearDirty();
51
+ options.outputLoadSampler.stop();
52
+ options.startupBackgroundProbeService.stop();
53
+ options.clearResizeTimer();
54
+ options.clearPtyResizeTimer();
55
+ options.clearHomePaneBackgroundTimer();
56
+ options.clearStorageLifecycleTimer();
57
+ options.clearProjectPaneGitHubReviewRefreshTimer();
58
+ options.persistMuxUiStateNow();
59
+ options.clearConversationTitleEditTimer();
60
+ options.flushTaskComposerPersist();
61
+ options.clearRenderScheduled();
62
+ options.detachProcessListeners();
63
+ options.removeEnvelopeListener();
64
+ options.stopWorkspaceObservedEvents();
65
+ await options.unsubscribeTaskPlanningEvents();
66
+ await options.closeKeyEventSubscription();
67
+ options.clearRuntimeFatalExitTimer();
68
+ try {
69
+ await options.waitForControlPlaneDrain();
70
+ await options.controlPlaneClient.close();
71
+ } catch {
72
+ // Best-effort shutdown only.
78
73
  }
74
+
75
+ options.eventPersistence.flush('shutdown');
76
+ const recordingCloseError = await options.recordingService.closeWriter();
77
+ options.store.close();
78
+ options.restoreTerminalState();
79
+ await options.recordingService.finalizeAfterShutdown(recordingCloseError);
80
+ options.startupShutdownService.finalize();
81
+ options.shutdownPerfCore();
79
82
  }