@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,7 +1,8 @@
1
1
  import type { WorkspaceModel } from '../domain/workspace.ts';
2
2
  import type { RepositoryManager } from '../domain/repositories.ts';
3
+ import type { ProjectPaneGitHubReviewSummary } from '../mux/project-pane-github-review.ts';
3
4
 
4
- interface RuntimeLeftRailRenderLayout {
5
+ export interface RuntimeLeftRailRenderLayout {
5
6
  readonly cols: number;
6
7
  readonly paneRows: number;
7
8
  readonly leftCols: number;
@@ -27,7 +28,6 @@ interface LeftRailPaneLike<
27
28
  TConversationRecord,
28
29
  TGitSummary,
29
30
  TProcessUsage,
30
- TShortcutBindings,
31
31
  TRailViewRows,
32
32
  > {
33
33
  render(input: {
@@ -44,12 +44,19 @@ interface LeftRailPaneLike<
44
44
  projectSelectionEnabled: boolean;
45
45
  repositorySelectionEnabled: boolean;
46
46
  homeSelectionEnabled: boolean;
47
+ nimSelectionEnabled: boolean;
48
+ tasksSelectionEnabled: boolean;
49
+ showTasksEntry: boolean;
50
+ showGitHubIntegration: boolean;
51
+ visibleGitHubDirectoryIds?: ReadonlySet<string>;
52
+ expandedGitHubDirectoryIds?: ReadonlySet<string>;
53
+ githubReviewByDirectoryId: ReadonlyMap<string, ProjectPaneGitHubReviewSummary>;
54
+ githubSelectionEnabled: boolean;
55
+ activeGitHubProjectId: string | null;
47
56
  repositoriesCollapsed: boolean;
48
57
  collapsedRepositoryGroupIds: ReadonlySet<string>;
49
- shortcutsCollapsed: boolean;
50
58
  gitSummaryByDirectoryId: ReadonlyMap<string, TGitSummary>;
51
59
  processUsageBySessionId: ReadonlyMap<string, TProcessUsage>;
52
- shortcutBindings: TShortcutBindings;
53
60
  loadingGitSummary: TGitSummary;
54
61
  }): {
55
62
  readonly ansiRows: readonly string[];
@@ -57,14 +64,27 @@ interface LeftRailPaneLike<
57
64
  };
58
65
  }
59
66
 
60
- interface RuntimeLeftRailRenderOptions<
67
+ export interface RuntimeLeftRailRenderSnapshot<
68
+ TDirectoryRecord,
69
+ TConversationRecord,
70
+ TRepositoryRecord,
71
+ TProcessUsage,
72
+ > {
73
+ readonly repositories: ReadonlyMap<string, TRepositoryRecord>;
74
+ readonly directories: ReadonlyMap<string, TDirectoryRecord>;
75
+ readonly conversations: ReadonlyMap<string, TConversationRecord>;
76
+ readonly orderedConversationIds: readonly string[];
77
+ readonly processUsageBySessionId: ReadonlyMap<string, TProcessUsage>;
78
+ readonly activeConversationId: string | null;
79
+ }
80
+
81
+ export interface RuntimeLeftRailRenderOptions<
61
82
  TDirectoryRecord,
62
83
  TConversationRecord,
63
84
  TRepositoryRecord,
64
85
  TRepositorySnapshot,
65
86
  TGitSummary,
66
87
  TProcessUsage,
67
- TShortcutBindings,
68
88
  TRailViewRows,
69
89
  > {
70
90
  readonly leftRailPane: LeftRailPaneLike<
@@ -75,7 +95,6 @@ interface RuntimeLeftRailRenderOptions<
75
95
  TConversationRecord,
76
96
  TGitSummary,
77
97
  TProcessUsage,
78
- TShortcutBindings,
79
98
  TRailViewRows
80
99
  >;
81
100
  readonly sessionProjectionInstrumentation: SessionProjectionInstrumentationLike<
@@ -84,76 +103,93 @@ interface RuntimeLeftRailRenderOptions<
84
103
  >;
85
104
  readonly workspace: WorkspaceModel;
86
105
  readonly repositoryManager: RepositoryManager<TRepositoryRecord, TRepositorySnapshot>;
87
- readonly repositories: ReadonlyMap<string, TRepositoryRecord>;
88
106
  readonly repositoryAssociationByDirectoryId: ReadonlyMap<string, string>;
89
107
  readonly directoryRepositorySnapshotByDirectoryId: ReadonlyMap<string, TRepositorySnapshot>;
90
- readonly directories: ReadonlyMap<string, TDirectoryRecord>;
91
- readonly conversations: ReadonlyMap<string, TConversationRecord>;
92
108
  readonly gitSummaryByDirectoryId: ReadonlyMap<string, TGitSummary>;
93
- readonly processUsageBySessionId: () => ReadonlyMap<string, TProcessUsage>;
94
- readonly shortcutBindings: TShortcutBindings;
95
109
  readonly loadingGitSummary: TGitSummary;
96
- readonly activeConversationId: () => string | null;
97
- readonly orderedConversationIds: () => readonly string[];
110
+ readonly showGitHubIntegration?: boolean;
111
+ readonly visibleGitHubDirectoryIds?: ReadonlySet<string>;
112
+ readonly expandedGitHubDirectoryIds?: ReadonlySet<string>;
113
+ readonly githubReviewByDirectoryId?: ReadonlyMap<string, ProjectPaneGitHubReviewSummary>;
114
+ readonly showTasksEntry?: boolean;
98
115
  }
99
116
 
100
- export class RuntimeLeftRailRender<
117
+ export function renderRuntimeLeftRail<
101
118
  TDirectoryRecord,
102
119
  TConversationRecord,
103
120
  TRepositoryRecord,
104
121
  TRepositorySnapshot,
105
122
  TGitSummary,
106
123
  TProcessUsage,
107
- TShortcutBindings,
108
124
  TRailViewRows,
109
- > {
110
- constructor(
111
- private readonly options: RuntimeLeftRailRenderOptions<
125
+ >(
126
+ options: RuntimeLeftRailRenderOptions<
127
+ TDirectoryRecord,
128
+ TConversationRecord,
129
+ TRepositoryRecord,
130
+ TRepositorySnapshot,
131
+ TGitSummary,
132
+ TProcessUsage,
133
+ TRailViewRows
134
+ >,
135
+ input: {
136
+ readonly layout: RuntimeLeftRailRenderLayout;
137
+ readonly snapshot: RuntimeLeftRailRenderSnapshot<
112
138
  TDirectoryRecord,
113
139
  TConversationRecord,
114
140
  TRepositoryRecord,
115
- TRepositorySnapshot,
116
- TGitSummary,
117
- TProcessUsage,
118
- TShortcutBindings,
119
- TRailViewRows
120
- >,
121
- ) {}
122
-
123
- render(layout: RuntimeLeftRailRenderLayout): {
124
- readonly ansiRows: readonly string[];
125
- readonly viewRows: TRailViewRows;
126
- } {
127
- const orderedIds = this.options.orderedConversationIds();
128
- this.options.sessionProjectionInstrumentation.refreshSelectorSnapshot(
129
- 'render',
130
- this.options.directories,
131
- this.options.conversations,
132
- orderedIds,
133
- );
134
- return this.options.leftRailPane.render({
135
- layout,
136
- repositories: this.options.repositories,
137
- repositoryAssociationByDirectoryId: this.options.repositoryAssociationByDirectoryId,
138
- directoryRepositorySnapshotByDirectoryId:
139
- this.options.directoryRepositorySnapshotByDirectoryId,
140
- directories: this.options.directories,
141
- conversations: this.options.conversations,
142
- orderedIds,
143
- activeProjectId: this.options.workspace.activeDirectoryId,
144
- activeRepositoryId: this.options.workspace.activeRepositorySelectionId,
145
- activeConversationId: this.options.activeConversationId(),
146
- projectSelectionEnabled: this.options.workspace.leftNavSelection.kind === 'project',
147
- repositorySelectionEnabled: this.options.workspace.leftNavSelection.kind === 'repository',
148
- homeSelectionEnabled: this.options.workspace.leftNavSelection.kind === 'home',
149
- repositoriesCollapsed: this.options.workspace.repositoriesCollapsed,
150
- collapsedRepositoryGroupIds:
151
- this.options.repositoryManager.readonlyCollapsedRepositoryGroupIds(),
152
- shortcutsCollapsed: this.options.workspace.shortcutsCollapsed,
153
- gitSummaryByDirectoryId: this.options.gitSummaryByDirectoryId,
154
- processUsageBySessionId: this.options.processUsageBySessionId(),
155
- shortcutBindings: this.options.shortcutBindings,
156
- loadingGitSummary: this.options.loadingGitSummary,
157
- });
158
- }
141
+ TProcessUsage
142
+ >;
143
+ },
144
+ ): {
145
+ readonly ansiRows: readonly string[];
146
+ readonly viewRows: TRailViewRows;
147
+ } {
148
+ options.sessionProjectionInstrumentation.refreshSelectorSnapshot(
149
+ 'render',
150
+ input.snapshot.directories,
151
+ input.snapshot.conversations,
152
+ input.snapshot.orderedConversationIds,
153
+ );
154
+ const renderInput = {
155
+ layout: input.layout,
156
+ repositories: input.snapshot.repositories,
157
+ repositoryAssociationByDirectoryId: options.repositoryAssociationByDirectoryId,
158
+ directoryRepositorySnapshotByDirectoryId: options.directoryRepositorySnapshotByDirectoryId,
159
+ directories: input.snapshot.directories,
160
+ conversations: input.snapshot.conversations,
161
+ orderedIds: input.snapshot.orderedConversationIds,
162
+ activeProjectId: options.workspace.activeDirectoryId,
163
+ activeRepositoryId: options.workspace.activeRepositorySelectionId,
164
+ activeConversationId: input.snapshot.activeConversationId,
165
+ projectSelectionEnabled: options.workspace.leftNavSelection.kind === 'project',
166
+ repositorySelectionEnabled: options.workspace.leftNavSelection.kind === 'repository',
167
+ homeSelectionEnabled: options.workspace.leftNavSelection.kind === 'home',
168
+ nimSelectionEnabled: options.workspace.leftNavSelection.kind === 'nim',
169
+ tasksSelectionEnabled: options.workspace.leftNavSelection.kind === 'tasks',
170
+ showTasksEntry: options.showTasksEntry ?? true,
171
+ showGitHubIntegration: options.showGitHubIntegration ?? false,
172
+ githubReviewByDirectoryId: options.githubReviewByDirectoryId ?? new Map(),
173
+ githubSelectionEnabled: options.workspace.leftNavSelection.kind === 'github',
174
+ activeGitHubProjectId:
175
+ options.workspace.leftNavSelection.kind === 'github'
176
+ ? options.workspace.leftNavSelection.directoryId
177
+ : null,
178
+ repositoriesCollapsed: options.workspace.repositoriesCollapsed,
179
+ collapsedRepositoryGroupIds: options.repositoryManager.readonlyCollapsedRepositoryGroupIds(),
180
+ gitSummaryByDirectoryId: options.gitSummaryByDirectoryId,
181
+ processUsageBySessionId: input.snapshot.processUsageBySessionId,
182
+ loadingGitSummary: options.loadingGitSummary,
183
+ ...(options.visibleGitHubDirectoryIds !== undefined
184
+ ? {
185
+ visibleGitHubDirectoryIds: options.visibleGitHubDirectoryIds,
186
+ }
187
+ : {}),
188
+ ...(options.expandedGitHubDirectoryIds !== undefined
189
+ ? {
190
+ expandedGitHubDirectoryIds: options.expandedGitHubDirectoryIds,
191
+ }
192
+ : {}),
193
+ };
194
+ return options.leftRailPane.render(renderInput);
159
195
  }
@@ -0,0 +1,438 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { setTimeout as delay } from 'node:timers/promises';
4
+ import { startPtySession, type PtyExit } from '../pty/pty_host.ts';
5
+ import type { NimModelRef } from '../../packages/nim-core/src/index.ts';
6
+ import type { RuntimeNimViewModel } from './runtime-nim-session.ts';
7
+
8
+ type NimSessionStatus = RuntimeNimViewModel['status'];
9
+
10
+ interface RuntimeNimCliSessionOptions {
11
+ readonly invocationDirectory: string;
12
+ readonly tenantId: string;
13
+ readonly userId: string;
14
+ readonly markDirty: () => void;
15
+ readonly sessionName: string | null;
16
+ readonly model: NimModelRef;
17
+ readonly useMock: boolean;
18
+ readonly baseUrl?: string;
19
+ readonly maxTranscriptLines?: number;
20
+ readonly harnessScriptPath?: string;
21
+ readonly env?: NodeJS.ProcessEnv;
22
+ readonly startPtySession?: typeof startPtySession;
23
+ }
24
+
25
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
26
+ const PROJECT_ROOT = resolve(MODULE_DIR, '../..');
27
+ const DEFAULT_HARNESS_SCRIPT_PATH = resolve(PROJECT_ROOT, 'scripts/harness.ts');
28
+ const DEFAULT_MAX_TRANSCRIPT_LINES = 200;
29
+ const DEFAULT_COLS = 100;
30
+ const DEFAULT_ROWS = 30;
31
+
32
+ function stripAnsiSequences(value: string): string {
33
+ const ESC = String.fromCharCode(27);
34
+ const BEL = String.fromCharCode(7);
35
+ const oscPattern = new RegExp(`${ESC}\\][^${BEL}]*${BEL}`, 'gu');
36
+ const csiPattern = new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]`, 'gu');
37
+ const escPattern = new RegExp(`${ESC}[@-_]`, 'gu');
38
+ return value.replace(oscPattern, '').replace(csiPattern, '').replace(escPattern, '');
39
+ }
40
+
41
+ function isPrintableCharacter(char: string): boolean {
42
+ return char.length === 1 && char >= ' ' && char !== '\u007f';
43
+ }
44
+
45
+ function providerIdFromModel(model: NimModelRef): string {
46
+ const slash = model.indexOf('/');
47
+ if (slash <= 0) {
48
+ return 'mock';
49
+ }
50
+ return model.slice(0, slash);
51
+ }
52
+
53
+ interface QueueTurnResultLine {
54
+ readonly queued: boolean;
55
+ readonly position?: number;
56
+ readonly reason?: string;
57
+ }
58
+
59
+ function parseQueueTurnResultLine(line: string): QueueTurnResultLine | null {
60
+ const trimmed = line.trim();
61
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
62
+ return null;
63
+ }
64
+ let parsed: unknown;
65
+ try {
66
+ parsed = JSON.parse(trimmed) as unknown;
67
+ } catch {
68
+ return null;
69
+ }
70
+ if (typeof parsed !== 'object' || parsed === null) {
71
+ return null;
72
+ }
73
+ const record = parsed as Record<string, unknown>;
74
+ if (typeof record['queued'] !== 'boolean') {
75
+ return null;
76
+ }
77
+ const positionValue = record['position'];
78
+ const reasonValue = record['reason'];
79
+ const position =
80
+ typeof positionValue === 'number' && Number.isInteger(positionValue) && positionValue >= 0
81
+ ? positionValue
82
+ : undefined;
83
+ const reason =
84
+ typeof reasonValue === 'string' && reasonValue.trim().length > 0 ? reasonValue : undefined;
85
+ return {
86
+ queued: record['queued'],
87
+ ...(position === undefined ? {} : { position }),
88
+ ...(reason === undefined ? {} : { reason }),
89
+ };
90
+ }
91
+
92
+ function parseQueueDepthLine(line: string): number | null {
93
+ const match = /^queue depth (\d+)$/u.exec(line.trim());
94
+ if (match === null) {
95
+ return null;
96
+ }
97
+ const parsed = Number.parseInt(match[1] ?? '', 10);
98
+ if (!Number.isInteger(parsed) || parsed < 0) {
99
+ return null;
100
+ }
101
+ return parsed;
102
+ }
103
+
104
+ function normalizePtyExit(value: unknown): PtyExit | null {
105
+ if (typeof value !== 'object' || value === null) {
106
+ return null;
107
+ }
108
+ const candidate = value as { code?: unknown; signal?: unknown };
109
+ if (
110
+ (typeof candidate.code !== 'number' && candidate.code !== null) ||
111
+ (typeof candidate.signal !== 'string' && candidate.signal !== null)
112
+ ) {
113
+ return null;
114
+ }
115
+ return {
116
+ code: candidate.code,
117
+ signal: typeof candidate.signal === 'string' ? (candidate.signal as NodeJS.Signals) : null,
118
+ };
119
+ }
120
+
121
+ export class RuntimeNimCliSession {
122
+ private readonly startPtySessionImpl: typeof startPtySession;
123
+ private readonly harnessScriptPath: string;
124
+ private readonly env: NodeJS.ProcessEnv;
125
+ private readonly maxTranscriptLines: number;
126
+
127
+ private started = false;
128
+ private disposed = false;
129
+ private session: ReturnType<typeof startPtySession> | null = null;
130
+ private sessionExitPromise: Promise<PtyExit> | null = null;
131
+ private pendingOutput = '';
132
+
133
+ private sessionId: string | null = null;
134
+ private status: NimSessionStatus = 'idle';
135
+ private uiMode: RuntimeNimViewModel['uiMode'] = 'debug';
136
+ private composerText = '';
137
+ private queuedCount = 0;
138
+ private transcriptLines: string[] = [];
139
+ private activeRunId: string | null = null;
140
+ private pendingDirectRunStarts = 0;
141
+
142
+ constructor(private readonly options: RuntimeNimCliSessionOptions) {
143
+ this.startPtySessionImpl = options.startPtySession ?? startPtySession;
144
+ this.harnessScriptPath = options.harnessScriptPath ?? DEFAULT_HARNESS_SCRIPT_PATH;
145
+ this.env = options.env ?? process.env;
146
+ this.maxTranscriptLines = options.maxTranscriptLines ?? DEFAULT_MAX_TRANSCRIPT_LINES;
147
+ }
148
+
149
+ public async start(): Promise<void> {
150
+ if (this.started || this.disposed) {
151
+ return;
152
+ }
153
+ this.started = true;
154
+ const commandArgs: string[] = [this.harnessScriptPath];
155
+ if (this.options.sessionName !== null) {
156
+ commandArgs.push('--session', this.options.sessionName);
157
+ }
158
+ commandArgs.push(
159
+ 'nim',
160
+ '--tenant-id',
161
+ this.options.tenantId,
162
+ '--user-id',
163
+ this.options.userId,
164
+ '--model',
165
+ this.options.model,
166
+ '--ui-mode',
167
+ 'debug',
168
+ );
169
+ if (this.options.useMock) {
170
+ commandArgs.push('--mock');
171
+ } else {
172
+ commandArgs.push('--live-anthropic');
173
+ if (typeof this.options.baseUrl === 'string' && this.options.baseUrl.trim().length > 0) {
174
+ commandArgs.push('--base-url', this.options.baseUrl.trim());
175
+ }
176
+ }
177
+ const session = this.startPtySessionImpl({
178
+ command: process.execPath,
179
+ commandArgs,
180
+ cwd: this.options.invocationDirectory,
181
+ env: {
182
+ ...this.env,
183
+ HARNESS_INVOKE_CWD: this.options.invocationDirectory,
184
+ },
185
+ initialCols: DEFAULT_COLS,
186
+ initialRows: DEFAULT_ROWS,
187
+ });
188
+ this.session = session;
189
+ this.sessionExitPromise = new Promise((resolveExit) => {
190
+ session.once('exit', (value: unknown) => {
191
+ const normalized = normalizePtyExit(value);
192
+ resolveExit(
193
+ normalized ?? {
194
+ code: 1,
195
+ signal: null,
196
+ },
197
+ );
198
+ });
199
+ });
200
+ session.on('data', (chunk: Buffer) => {
201
+ this.applyOutputChunk(chunk.toString('utf8'));
202
+ });
203
+ this.options.markDirty();
204
+ }
205
+
206
+ public async dispose(): Promise<void> {
207
+ this.disposed = true;
208
+ const session = this.session;
209
+ const exitPromise = this.sessionExitPromise;
210
+ this.session = null;
211
+ this.sessionExitPromise = null;
212
+ if (session === null) {
213
+ return;
214
+ }
215
+ try {
216
+ session.write('/exit\n');
217
+ } catch {
218
+ // Best-effort shutdown only.
219
+ }
220
+ if (exitPromise !== null) {
221
+ const exited = await Promise.race([
222
+ exitPromise.then(() => true),
223
+ delay(500).then(() => false),
224
+ ]);
225
+ if (exited) {
226
+ return;
227
+ }
228
+ }
229
+ try {
230
+ session.close();
231
+ } catch {
232
+ // Best-effort shutdown only.
233
+ }
234
+ if (exitPromise !== null) {
235
+ await Promise.race([exitPromise, delay(500)]);
236
+ }
237
+ }
238
+
239
+ public snapshot(): RuntimeNimViewModel {
240
+ return {
241
+ sessionId: this.sessionId,
242
+ status: this.status,
243
+ uiMode: this.uiMode,
244
+ composerText: this.composerText,
245
+ queuedCount: this.queuedCount,
246
+ activeRunId: this.activeRunId,
247
+ transcriptLines: this.transcriptLines,
248
+ assistantDraftText: '',
249
+ };
250
+ }
251
+
252
+ public handleInputChunk(text: string): void {
253
+ if (text.length === 0 || this.disposed) {
254
+ return;
255
+ }
256
+ let skipLf = false;
257
+ for (const char of text) {
258
+ if (skipLf && char === '\n') {
259
+ skipLf = false;
260
+ continue;
261
+ }
262
+ skipLf = false;
263
+ if (char === '\r' || char === '\n') {
264
+ this.flushComposerAsSend();
265
+ if (char === '\r') {
266
+ skipLf = true;
267
+ }
268
+ continue;
269
+ }
270
+ if (char === '\t') {
271
+ this.flushComposerAsQueue();
272
+ continue;
273
+ }
274
+ if (char === '\u007f' || char === '\b') {
275
+ this.composerText = this.composerText.slice(0, -1);
276
+ continue;
277
+ }
278
+ if (!isPrintableCharacter(char)) {
279
+ continue;
280
+ }
281
+ this.composerText += char;
282
+ }
283
+ this.options.markDirty();
284
+ }
285
+
286
+ private flushComposerAsSend(): void {
287
+ const message = this.composerText.trim();
288
+ this.composerText = '';
289
+ if (message.length === 0 || this.session === null) {
290
+ return;
291
+ }
292
+ this.pendingDirectRunStarts += 1;
293
+ this.session.write(`${message}\n`);
294
+ }
295
+
296
+ private flushComposerAsQueue(): void {
297
+ const message = this.composerText.trim();
298
+ this.composerText = '';
299
+ if (message.length === 0 || this.session === null) {
300
+ return;
301
+ }
302
+ const queueCommand =
303
+ message.startsWith('/queue ') || message === '/queue' ? message : `/queue ${message}`;
304
+ this.session.write(`${queueCommand}\n`);
305
+ }
306
+
307
+ public handleEscape(): void {
308
+ if (this.disposed) {
309
+ return;
310
+ }
311
+ if (this.session !== null) {
312
+ this.session.write('/abort\n');
313
+ }
314
+ this.status = 'idle';
315
+ this.options.markDirty();
316
+ }
317
+
318
+ public resize(cols: number, rows: number): void {
319
+ if (this.disposed || this.session === null) {
320
+ return;
321
+ }
322
+ this.session.resize(Math.max(20, Math.floor(cols)), Math.max(6, Math.floor(rows)));
323
+ }
324
+
325
+ private applyOutputChunk(text: string): void {
326
+ const stripped = stripAnsiSequences(text).replace(/\r/gu, '');
327
+ if (stripped.length === 0) {
328
+ return;
329
+ }
330
+ this.pendingOutput += stripped;
331
+ while (true) {
332
+ const newlineIndex = this.pendingOutput.indexOf('\n');
333
+ if (newlineIndex < 0) {
334
+ break;
335
+ }
336
+ const rawLine = this.pendingOutput.slice(0, newlineIndex);
337
+ this.pendingOutput = this.pendingOutput.slice(newlineIndex + 1);
338
+ this.applyOutputLine(rawLine.trim());
339
+ }
340
+ this.options.markDirty();
341
+ }
342
+
343
+ private applyOutputLine(line: string): void {
344
+ if (line.length === 0) {
345
+ return;
346
+ }
347
+ const readyMatch = /^nim tui ready session=([^\s]+)/u.exec(line);
348
+ if (readyMatch !== null) {
349
+ this.sessionId = readyMatch[1] ?? this.sessionId;
350
+ const providerId = providerIdFromModel(this.options.model);
351
+ this.pushTranscriptLine(
352
+ `[notice] nim subprocess ready model=${this.options.model} provider=${providerId}`,
353
+ );
354
+ return;
355
+ }
356
+ if (line.startsWith('run started ')) {
357
+ const runId = line.slice('run started '.length).trim();
358
+ this.activeRunId = runId.length > 0 ? runId : null;
359
+ this.status = 'thinking';
360
+ if (this.pendingDirectRunStarts > 0) {
361
+ this.pendingDirectRunStarts -= 1;
362
+ } else if (this.queuedCount > 0) {
363
+ this.queuedCount = Math.max(0, this.queuedCount - 1);
364
+ }
365
+ this.pushTranscriptLine(line);
366
+ return;
367
+ }
368
+ if (line.startsWith('run completed ')) {
369
+ this.activeRunId = null;
370
+ this.status = 'idle';
371
+ this.pushTranscriptLine(line);
372
+ return;
373
+ }
374
+ if (line.startsWith('ui mode set to ')) {
375
+ const mode = line.slice('ui mode set to '.length).trim();
376
+ if (mode === 'debug' || mode === 'user') {
377
+ this.uiMode = mode;
378
+ }
379
+ this.pushTranscriptLine(`[notice] ${line}`);
380
+ return;
381
+ }
382
+ if (line.startsWith('new session ') || line.startsWith('resumed session ')) {
383
+ const sessionId = line.split(' ').at(-1) ?? '';
384
+ if (sessionId.length > 0) {
385
+ this.sessionId = sessionId;
386
+ }
387
+ this.queuedCount = 0;
388
+ this.pendingDirectRunStarts = 0;
389
+ this.pushTranscriptLine(`[notice] ${line}`);
390
+ return;
391
+ }
392
+ const queueDepth = parseQueueDepthLine(line);
393
+ if (queueDepth !== null) {
394
+ this.queuedCount = queueDepth;
395
+ return;
396
+ }
397
+ if (line === 'frame:') {
398
+ this.status = 'responding';
399
+ return;
400
+ }
401
+ if (line.startsWith('[error]')) {
402
+ this.status = 'idle';
403
+ this.activeRunId = null;
404
+ this.pendingDirectRunStarts = 0;
405
+ }
406
+ const queuedTurnResult = parseQueueTurnResultLine(line);
407
+ if (queuedTurnResult !== null) {
408
+ if (queuedTurnResult.queued) {
409
+ const nextQueuedCount =
410
+ queuedTurnResult.position === undefined
411
+ ? this.queuedCount + 1
412
+ : queuedTurnResult.position + 1;
413
+ this.queuedCount = Math.max(this.queuedCount, nextQueuedCount);
414
+ this.pushTranscriptLine(
415
+ `[notice] queued turn position=${String(
416
+ queuedTurnResult.position === undefined
417
+ ? this.queuedCount - 1
418
+ : queuedTurnResult.position,
419
+ )}`,
420
+ );
421
+ return;
422
+ }
423
+ this.pushTranscriptLine(
424
+ `[notice] queue rejected${queuedTurnResult.reason === undefined ? '' : ` reason=${queuedTurnResult.reason}`}`,
425
+ );
426
+ return;
427
+ }
428
+ this.pushTranscriptLine(line);
429
+ }
430
+
431
+ private pushTranscriptLine(text: string): void {
432
+ this.transcriptLines.push(text);
433
+ const overflow = this.transcriptLines.length - this.maxTranscriptLines;
434
+ if (overflow > 0) {
435
+ this.transcriptLines.splice(0, overflow);
436
+ }
437
+ }
438
+ }