@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,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,53 +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
- this.options.workspace.apiKeyPrompt = null;
59
- if (this.options.workspace.conversationTitleEdit !== null) {
60
- 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();
61
90
  }
62
- this.options.workspace.conversationTitleEditClickState = null;
63
- this.options.workspace.repositoryPrompt = {
91
+ options.workspace.conversationTitleEditClickState = null;
92
+ options.workspace.repositoryPrompt = {
64
93
  mode: 'add',
65
94
  repositoryId: null,
66
95
  value: '',
67
96
  error: null,
68
97
  };
69
- this.options.markDirty();
70
- }
98
+ options.markDirty();
99
+ };
71
100
 
72
- openRepositoryPromptForEdit(repositoryId: string): void {
73
- const repository = this.options.repositories.get(repositoryId);
101
+ const openRepositoryPromptForEdit = (repositoryId: string): void => {
102
+ const repository = options.repositories.get(repositoryId);
74
103
  if (repository === undefined) {
75
104
  return;
76
105
  }
77
- this.options.workspace.newThreadPrompt = null;
78
- this.options.workspace.addDirectoryPrompt = null;
79
- this.options.workspace.apiKeyPrompt = null;
80
- if (this.options.workspace.conversationTitleEdit !== null) {
81
- 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();
82
111
  }
83
- this.options.workspace.conversationTitleEditClickState = null;
84
- this.options.workspace.repositoryPrompt = {
112
+ options.workspace.conversationTitleEditClickState = null;
113
+ options.workspace.repositoryPrompt = {
85
114
  mode: 'edit',
86
115
  repositoryId,
87
116
  value: repository.remoteUrl,
88
117
  error: null,
89
118
  };
90
- this.options.workspace.taskPaneSelectionFocus = 'repository';
91
- this.options.markDirty();
92
- }
119
+ options.workspace.taskPaneSelectionFocus = 'repository';
120
+ options.markDirty();
121
+ };
93
122
 
94
- queueRepositoryPriorityOrder(orderedRepositoryIds: readonly string[], label: string): void {
123
+ const queueRepositoryPriorityOrder = (
124
+ orderedRepositoryIds: readonly string[],
125
+ label: string,
126
+ ): void => {
95
127
  const updates: Array<{ repositoryId: string; metadata: Record<string, unknown> }> = [];
96
128
  for (let index = 0; index < orderedRepositoryIds.length; index += 1) {
97
129
  const repositoryId = orderedRepositoryIds[index]!;
98
- const repository = this.options.repositories.get(repositoryId);
130
+ const repository = options.repositories.get(repositoryId);
99
131
  if (repository === undefined) {
100
132
  continue;
101
133
  }
@@ -113,25 +145,25 @@ export class RuntimeRepositoryActions<TRepository extends RepositoryRecordShape>
113
145
  if (updates.length === 0) {
114
146
  return;
115
147
  }
116
- this.options.queueControlPlaneOp(async () => {
148
+ options.queueControlPlaneOp(async () => {
117
149
  for (const update of updates) {
118
- const repository = await this.options.controlPlaneService.updateRepository({
150
+ const repository = await options.controlPlaneService.updateRepository({
119
151
  repositoryId: update.repositoryId,
120
152
  metadata: update.metadata,
121
153
  });
122
- this.options.repositories.set(repository.repositoryId, repository);
154
+ options.repositories.set(repository.repositoryId, repository);
123
155
  }
124
- this.options.syncTaskPaneRepositorySelection();
125
- this.options.markDirty();
156
+ options.syncTaskPaneRepositorySelection();
157
+ options.markDirty();
126
158
  }, label);
127
- }
159
+ };
128
160
 
129
- reorderRepositoryByDrop(
161
+ const reorderRepositoryByDrop = (
130
162
  draggedRepositoryId: string,
131
163
  targetRepositoryId: string,
132
164
  orderedRepositoryIds: readonly string[],
133
- ): void {
134
- const reordered = this.reorderIdsByMove(
165
+ ): void => {
166
+ const reordered = reorderIdsByMove(
135
167
  orderedRepositoryIds,
136
168
  draggedRepositoryId,
137
169
  targetRepositoryId,
@@ -139,22 +171,22 @@ export class RuntimeRepositoryActions<TRepository extends RepositoryRecordShape>
139
171
  if (reordered === null) {
140
172
  return;
141
173
  }
142
- this.queueRepositoryPriorityOrder(reordered, 'repositories-reorder-drag');
143
- }
174
+ queueRepositoryPriorityOrder(reordered, 'repositories-reorder-drag');
175
+ };
144
176
 
145
- async upsertRepositoryByRemoteUrl(
177
+ const upsertRepositoryByRemoteUrl = async (
146
178
  remoteUrl: string,
147
179
  existingRepositoryId?: string,
148
- ): Promise<void> {
149
- const normalizedRemoteUrl = this.options.normalizeGitHubRemoteUrl(remoteUrl);
180
+ ): Promise<void> => {
181
+ const normalizedRemoteUrl = options.normalizeGitHubRemoteUrl(remoteUrl);
150
182
  if (normalizedRemoteUrl === null) {
151
183
  throw new Error('github url required');
152
184
  }
153
- const repositoryName = this.options.repositoryNameFromGitHubRemoteUrl(normalizedRemoteUrl);
185
+ const repositoryName = options.repositoryNameFromGitHubRemoteUrl(normalizedRemoteUrl);
154
186
  const repository =
155
187
  existingRepositoryId === undefined
156
- ? await this.options.controlPlaneService.upsertRepository({
157
- repositoryId: this.options.createRepositoryId(),
188
+ ? await options.controlPlaneService.upsertRepository({
189
+ repositoryId: options.createRepositoryId(),
158
190
  name: repositoryName,
159
191
  remoteUrl: normalizedRemoteUrl,
160
192
  defaultBranch: 'main',
@@ -162,38 +194,31 @@ export class RuntimeRepositoryActions<TRepository extends RepositoryRecordShape>
162
194
  source: 'mux-manual',
163
195
  },
164
196
  })
165
- : await this.options.controlPlaneService.updateRepository({
197
+ : await options.controlPlaneService.updateRepository({
166
198
  repositoryId: existingRepositoryId,
167
199
  name: repositoryName,
168
200
  remoteUrl: normalizedRemoteUrl,
169
201
  });
170
- this.options.repositories.set(repository.repositoryId, repository);
171
- this.options.syncRepositoryAssociationsWithDirectorySnapshots();
172
- this.options.syncTaskPaneRepositorySelection();
173
- this.options.markDirty();
174
- }
202
+ options.repositories.set(repository.repositoryId, repository);
203
+ options.syncRepositoryAssociationsWithDirectorySnapshots();
204
+ options.syncTaskPaneRepositorySelection();
205
+ options.markDirty();
206
+ };
175
207
 
176
- async archiveRepositoryById(repositoryId: string): Promise<void> {
177
- await this.options.controlPlaneService.archiveRepository(repositoryId);
178
- this.options.repositories.delete(repositoryId);
179
- this.options.syncRepositoryAssociationsWithDirectorySnapshots();
180
- this.options.syncTaskPaneRepositorySelection();
181
- this.options.markDirty();
182
- }
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
+ };
183
215
 
184
- private reorderIdsByMove(
185
- orderedIds: readonly string[],
186
- movedId: string,
187
- targetId: string,
188
- ): readonly string[] | null {
189
- const fromIndex = orderedIds.indexOf(movedId);
190
- const targetIndex = orderedIds.indexOf(targetId);
191
- if (fromIndex < 0 || targetIndex < 0 || fromIndex === targetIndex) {
192
- return null;
193
- }
194
- const reordered = [...orderedIds];
195
- const moved = reordered.splice(fromIndex, 1)[0]!;
196
- reordered.splice(targetIndex, 0, moved);
197
- return reordered;
198
- }
216
+ return {
217
+ openRepositoryPromptForCreate,
218
+ openRepositoryPromptForEdit,
219
+ queueRepositoryPriorityOrder,
220
+ reorderRepositoryByDrop,
221
+ upsertRepositoryByRemoteUrl,
222
+ archiveRepositoryById,
223
+ };
199
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
  }