@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
@@ -0,0 +1,748 @@
1
+ import { resolve } from 'node:path';
2
+ import { createInterface } from 'node:readline/promises';
3
+ import { loadHarnessSecrets } from '../src/config/secrets-core.ts';
4
+ import { resolveHarnessRuntimePath } from '../src/config/harness-paths.ts';
5
+ import {
6
+ createAnthropicNimProviderDriver,
7
+ createSqliteBackedNimRuntime,
8
+ type InMemoryNimRuntime,
9
+ type NimModelRef,
10
+ type SessionHandle,
11
+ } from '../packages/nim-core/src/index.ts';
12
+ import { NimTestTuiController } from '../packages/nim-test-tui/src/index.ts';
13
+ import type { NimUiMode } from '../packages/nim-ui-core/src/projection.ts';
14
+ import type { NimEventEnvelope } from '../packages/nim-core/src/events.ts';
15
+
16
+ type ParsedArgs = {
17
+ readonly tenantId: string;
18
+ readonly userId: string;
19
+ readonly model: NimModelRef;
20
+ readonly uiMode: NimUiMode;
21
+ readonly liveAnthropic: boolean;
22
+ readonly sessionId?: string;
23
+ readonly eventStorePath: string;
24
+ readonly sessionStorePath: string;
25
+ readonly telemetryPath?: string;
26
+ readonly secretsFile?: string;
27
+ readonly baseUrl?: string;
28
+ };
29
+
30
+ interface NimTuiParseOptions {
31
+ readonly cwd?: string;
32
+ readonly env?: NodeJS.ProcessEnv;
33
+ readonly sessionName?: string | null;
34
+ }
35
+
36
+ interface RunNimTuiSmokeOptions {
37
+ readonly sessionName?: string | null;
38
+ }
39
+
40
+ type Command =
41
+ | { readonly type: 'help' }
42
+ | { readonly type: 'exit' }
43
+ | { readonly type: 'send'; readonly text: string }
44
+ | { readonly type: 'steer'; readonly text: string }
45
+ | { readonly type: 'queue'; readonly text: string; readonly priority: 'normal' | 'high' }
46
+ | { readonly type: 'abort' }
47
+ | { readonly type: 'state' }
48
+ | { readonly type: 'replay'; readonly count: number }
49
+ | { readonly type: 'mode'; readonly mode: NimUiMode }
50
+ | { readonly type: 'switch-model'; readonly model: NimModelRef }
51
+ | { readonly type: 'session-new' }
52
+ | { readonly type: 'session-resume'; readonly sessionId: string };
53
+
54
+ function queuedCountFromReplayEvents(events: readonly NimEventEnvelope[]): number {
55
+ let queuedCount = 0;
56
+ for (const event of events) {
57
+ if (event.type === 'turn.queue.enqueued') {
58
+ const position = event.queue_position;
59
+ if (typeof position === 'number' && Number.isInteger(position) && position >= 0) {
60
+ queuedCount = Math.max(queuedCount, position + 1);
61
+ } else {
62
+ queuedCount += 1;
63
+ }
64
+ continue;
65
+ }
66
+ if (event.type === 'turn.queue.dequeued') {
67
+ queuedCount = Math.max(0, queuedCount - 1);
68
+ }
69
+ }
70
+ return queuedCount;
71
+ }
72
+
73
+ function printUsage(): void {
74
+ process.stdout.write(
75
+ [
76
+ 'usage:',
77
+ ' harness nim [options]',
78
+ '',
79
+ 'options:',
80
+ ' --tenant-id <id>',
81
+ ' --user-id <id>',
82
+ ' --model <provider/model>',
83
+ ' --ui-mode <debug|user>',
84
+ ' --live-anthropic',
85
+ ' --mock',
86
+ ' --session-id <id>',
87
+ ' --event-store-path <path>',
88
+ ' --session-store-path <path>',
89
+ ' --telemetry-path <path>',
90
+ ' --no-telemetry',
91
+ ' --secrets-file <path>',
92
+ ' --base-url <url>',
93
+ '',
94
+ 'notes:',
95
+ ' - Live Anthropic is the default path.',
96
+ ' - Use --mock for deterministic mock-echo provider output.',
97
+ ' - Live Anthropic requires ANTHROPIC_API_KEY (optionally loaded via --secrets-file).',
98
+ ].join('\n') + '\n',
99
+ );
100
+ }
101
+
102
+ export function parseNimTuiArgs(
103
+ argv: readonly string[],
104
+ input: NimTuiParseOptions = {},
105
+ ): ParsedArgs {
106
+ const cwd = input.cwd ?? process.cwd();
107
+ const env = input.env ?? process.env;
108
+ const sessionName = input.sessionName ?? null;
109
+ let tenantId = 'nim-tui-tenant';
110
+ let userId = 'nim-tui-user';
111
+ let model: NimModelRef = 'anthropic/claude-3-haiku-20240307';
112
+ let uiMode: NimUiMode = 'debug';
113
+ let liveAnthropic = true;
114
+ let sessionId: string | undefined;
115
+ const runtimeRoot =
116
+ sessionName === null ? '.harness/nim' : `.harness/sessions/${sessionName}/nim`;
117
+ let eventStorePath = resolveHarnessRuntimePath(cwd, `${runtimeRoot}/events.sqlite`, env);
118
+ let sessionStorePath = resolveHarnessRuntimePath(cwd, `${runtimeRoot}/sessions.sqlite`, env);
119
+ let telemetryPath: string | undefined = resolveHarnessRuntimePath(
120
+ cwd,
121
+ `${runtimeRoot}/events.jsonl`,
122
+ env,
123
+ );
124
+ let secretsFile: string | undefined;
125
+ let baseUrl: string | undefined;
126
+
127
+ for (let index = 0; index < argv.length; index += 1) {
128
+ const arg = argv[index];
129
+ const next = argv[index + 1];
130
+ if (arg === '--tenant-id') {
131
+ tenantId = requireArg(next, '--tenant-id');
132
+ index += 1;
133
+ continue;
134
+ }
135
+ if (arg === '--user-id') {
136
+ userId = requireArg(next, '--user-id');
137
+ index += 1;
138
+ continue;
139
+ }
140
+ if (arg === '--model') {
141
+ model = parseModelRef(requireArg(next, '--model'));
142
+ index += 1;
143
+ continue;
144
+ }
145
+ if (arg === '--ui-mode') {
146
+ const rawMode = requireArg(next, '--ui-mode');
147
+ uiMode = parseCliUiMode(rawMode, '--ui-mode');
148
+ index += 1;
149
+ continue;
150
+ }
151
+ if (arg === '--live-anthropic') {
152
+ liveAnthropic = true;
153
+ continue;
154
+ }
155
+ if (arg === '--mock') {
156
+ liveAnthropic = false;
157
+ continue;
158
+ }
159
+ if (arg === '--session-id') {
160
+ sessionId = requireArg(next, '--session-id');
161
+ index += 1;
162
+ continue;
163
+ }
164
+ if (arg === '--event-store-path') {
165
+ eventStorePath = resolve(cwd, requireArg(next, '--event-store-path'));
166
+ index += 1;
167
+ continue;
168
+ }
169
+ if (arg === '--session-store-path') {
170
+ sessionStorePath = resolve(cwd, requireArg(next, '--session-store-path'));
171
+ index += 1;
172
+ continue;
173
+ }
174
+ if (arg === '--telemetry-path') {
175
+ telemetryPath = resolve(cwd, requireArg(next, '--telemetry-path'));
176
+ index += 1;
177
+ continue;
178
+ }
179
+ if (arg === '--no-telemetry') {
180
+ telemetryPath = undefined;
181
+ continue;
182
+ }
183
+ if (arg === '--secrets-file') {
184
+ secretsFile = requireArg(next, '--secrets-file');
185
+ index += 1;
186
+ continue;
187
+ }
188
+ if (arg === '--base-url') {
189
+ baseUrl = requireArg(next, '--base-url');
190
+ index += 1;
191
+ continue;
192
+ }
193
+ throw new Error(`unknown argument: ${arg}`);
194
+ }
195
+
196
+ return {
197
+ tenantId,
198
+ userId,
199
+ model,
200
+ uiMode,
201
+ liveAnthropic,
202
+ ...(sessionId !== undefined ? { sessionId } : {}),
203
+ eventStorePath,
204
+ sessionStorePath,
205
+ ...(telemetryPath !== undefined ? { telemetryPath } : {}),
206
+ ...(secretsFile !== undefined ? { secretsFile } : {}),
207
+ ...(baseUrl !== undefined ? { baseUrl } : {}),
208
+ };
209
+ }
210
+
211
+ function requireArg(value: string | undefined, flag: string): string {
212
+ if (value === undefined) {
213
+ throw new Error(`missing value for ${flag}`);
214
+ }
215
+ return value;
216
+ }
217
+
218
+ function parseModelRef(value: string): NimModelRef {
219
+ const normalized = value.trim();
220
+ if (!/^[^/]+\/[^/]+$/u.test(normalized)) {
221
+ throw new Error(`invalid model ref: ${value}`);
222
+ }
223
+ return normalized as NimModelRef;
224
+ }
225
+
226
+ function parseCliUiMode(value: string, origin: '--ui-mode' | '/mode'): NimUiMode {
227
+ const normalized = value.trim();
228
+ if (normalized === 'debug') {
229
+ return 'debug';
230
+ }
231
+ if (normalized === 'user' || normalized === 'seamless') {
232
+ return 'seamless';
233
+ }
234
+ const prefix = origin === '--ui-mode' ? 'invalid --ui-mode' : 'invalid mode';
235
+ throw new Error(`${prefix}: ${value}`);
236
+ }
237
+
238
+ function uiModeLabel(mode: NimUiMode): 'debug' | 'user' {
239
+ return mode === 'debug' ? 'debug' : 'user';
240
+ }
241
+
242
+ export function parseNimTuiCommand(input: string): Command {
243
+ const trimmed = input.trim();
244
+ if (trimmed.length === 0) {
245
+ throw new Error('empty command');
246
+ }
247
+ if (!trimmed.startsWith('/')) {
248
+ return {
249
+ type: 'send',
250
+ text: trimmed,
251
+ };
252
+ }
253
+ if (trimmed === '/help') {
254
+ return { type: 'help' };
255
+ }
256
+ if (trimmed === '/exit' || trimmed === '/quit') {
257
+ return { type: 'exit' };
258
+ }
259
+ if (trimmed === '/abort') {
260
+ return { type: 'abort' };
261
+ }
262
+ if (trimmed === '/state') {
263
+ return { type: 'state' };
264
+ }
265
+ if (trimmed === '/session new') {
266
+ return { type: 'session-new' };
267
+ }
268
+ if (trimmed.startsWith('/session resume ')) {
269
+ const sessionId = trimmed.slice('/session resume '.length).trim();
270
+ if (sessionId.length === 0) {
271
+ throw new Error('missing session id for /session resume');
272
+ }
273
+ return {
274
+ type: 'session-resume',
275
+ sessionId,
276
+ };
277
+ }
278
+ if (trimmed.startsWith('/send ')) {
279
+ const text = trimmed.slice('/send '.length).trim();
280
+ if (text.length === 0) {
281
+ throw new Error('missing text for /send');
282
+ }
283
+ return {
284
+ type: 'send',
285
+ text,
286
+ };
287
+ }
288
+ if (trimmed.startsWith('/steer ')) {
289
+ const text = trimmed.slice('/steer '.length).trim();
290
+ if (text.length === 0) {
291
+ throw new Error('missing text for /steer');
292
+ }
293
+ return {
294
+ type: 'steer',
295
+ text,
296
+ };
297
+ }
298
+ if (trimmed.startsWith('/queue ')) {
299
+ const body = trimmed.slice('/queue '.length).trim();
300
+ if (body.length === 0) {
301
+ throw new Error('missing text for /queue');
302
+ }
303
+ if (body.startsWith('high ')) {
304
+ const text = body.slice('high '.length).trim();
305
+ if (text.length === 0) {
306
+ throw new Error('missing text for /queue high');
307
+ }
308
+ return { type: 'queue', text, priority: 'high' };
309
+ }
310
+ if (body.startsWith('normal ')) {
311
+ const text = body.slice('normal '.length).trim();
312
+ if (text.length === 0) {
313
+ throw new Error('missing text for /queue normal');
314
+ }
315
+ return { type: 'queue', text, priority: 'normal' };
316
+ }
317
+ return { type: 'queue', text: body, priority: 'normal' };
318
+ }
319
+ if (trimmed.startsWith('/replay')) {
320
+ const countRaw = trimmed.slice('/replay'.length).trim();
321
+ if (countRaw.length === 0) {
322
+ return { type: 'replay', count: 30 };
323
+ }
324
+ const parsed = Number.parseInt(countRaw, 10);
325
+ if (!Number.isInteger(parsed) || parsed <= 0) {
326
+ throw new Error(`invalid replay count: ${countRaw}`);
327
+ }
328
+ return { type: 'replay', count: parsed };
329
+ }
330
+ if (trimmed.startsWith('/mode ')) {
331
+ const raw = trimmed.slice('/mode '.length).trim();
332
+ return {
333
+ type: 'mode',
334
+ mode: parseCliUiMode(raw, '/mode'),
335
+ };
336
+ }
337
+ if (trimmed.startsWith('/model ')) {
338
+ const model = parseModelRef(trimmed.slice('/model '.length));
339
+ return {
340
+ type: 'switch-model',
341
+ model,
342
+ };
343
+ }
344
+ throw new Error(`unknown command: ${trimmed}`);
345
+ }
346
+
347
+ function printHelp(): void {
348
+ process.stdout.write(
349
+ [
350
+ 'nim tui commands',
351
+ ' /help',
352
+ ' /exit',
353
+ ' /send <text> (plain text without slash also sends)',
354
+ ' /steer <text> (append input to active run)',
355
+ ' /abort (abort active run)',
356
+ ' /queue [high|normal] <text>',
357
+ ' /replay [count]',
358
+ ' /state',
359
+ ' /mode <debug|user>',
360
+ ' /model <provider/model>',
361
+ ' /session new',
362
+ ' /session resume <session-id>',
363
+ '',
364
+ ].join('\n'),
365
+ );
366
+ }
367
+
368
+ async function nextWithTimeout<T>(
369
+ iterator: AsyncIterator<T>,
370
+ timeoutMs: number,
371
+ ): Promise<IteratorResult<T>> {
372
+ let timer: ReturnType<typeof setTimeout> | undefined;
373
+ try {
374
+ return await Promise.race([
375
+ iterator.next(),
376
+ new Promise<IteratorResult<T>>((_, reject) => {
377
+ timer = setTimeout(() => {
378
+ reject(new Error('timed out waiting for nim run events'));
379
+ }, timeoutMs);
380
+ }),
381
+ ]);
382
+ } finally {
383
+ if (timer !== undefined) {
384
+ clearTimeout(timer);
385
+ }
386
+ }
387
+ }
388
+
389
+ async function collectTurnTrace(input: {
390
+ runtime: InMemoryNimRuntime;
391
+ tenantId: string;
392
+ sessionId: string;
393
+ runId: string;
394
+ fromEventIdExclusive?: string;
395
+ uiMode: NimUiMode;
396
+ }): Promise<{ readonly lastEventId?: string; readonly frameLines: readonly string[] }> {
397
+ const stream = input.runtime.streamEvents({
398
+ tenantId: input.tenantId,
399
+ sessionId: input.sessionId,
400
+ ...(input.fromEventIdExclusive !== undefined
401
+ ? { fromEventIdExclusive: input.fromEventIdExclusive }
402
+ : {}),
403
+ includeThoughtDeltas: true,
404
+ includeToolArgumentDeltas: true,
405
+ });
406
+ const iterator = stream[Symbol.asyncIterator]();
407
+ const controller = new NimTestTuiController({
408
+ mode: input.uiMode,
409
+ runId: input.runId,
410
+ });
411
+ const deadline = Date.now() + 30000;
412
+ let lastEventId: string | undefined;
413
+ try {
414
+ while (Date.now() < deadline) {
415
+ const next = await nextWithTimeout(iterator, deadline - Date.now());
416
+ if (next.done) {
417
+ break;
418
+ }
419
+ const event = next.value;
420
+ lastEventId = event.event_id;
421
+ const stateLabel = event.state !== undefined ? ` state=${event.state}` : '';
422
+ process.stdout.write(
423
+ `[event ${String(event.event_seq)}] ${event.type} source=${event.source}${stateLabel}\n`,
424
+ );
425
+ const projected = controller.consume(event);
426
+ for (const item of projected) {
427
+ if (item.type === 'assistant.text.message') {
428
+ process.stdout.write(`assistant> ${item.text}\n`);
429
+ }
430
+ }
431
+ if (event.type === 'turn.completed' && event.run_id === input.runId) {
432
+ break;
433
+ }
434
+ }
435
+ return {
436
+ ...(lastEventId !== undefined ? { lastEventId } : {}),
437
+ frameLines: controller.snapshot().lines,
438
+ };
439
+ } finally {
440
+ await iterator.return?.();
441
+ }
442
+ }
443
+
444
+ async function runNimTuiInteractive(args: ParsedArgs): Promise<void> {
445
+ const runtimeHandle = createSqliteBackedNimRuntime({
446
+ eventStorePath: args.eventStorePath,
447
+ sessionStorePath: args.sessionStorePath,
448
+ ...(args.telemetryPath !== undefined
449
+ ? {
450
+ telemetry: {
451
+ filePath: args.telemetryPath,
452
+ mode: 'append',
453
+ } as const,
454
+ }
455
+ : {}),
456
+ });
457
+ const runtime = runtimeHandle.runtime;
458
+ const providerId = args.model.split('/')[0] ?? 'anthropic';
459
+ runtime.registerProvider({
460
+ id: providerId,
461
+ displayName: providerId,
462
+ models: [args.model],
463
+ });
464
+ runtime.registerTools([
465
+ { name: 'ping', description: 'Echo input' },
466
+ { name: 'note', description: 'Record note' },
467
+ { name: 'clock', description: 'Return current time' },
468
+ ]);
469
+ runtime.setToolPolicy({
470
+ hash: 'policy-cli-open',
471
+ allow: ['ping', 'note', 'clock'],
472
+ deny: [],
473
+ });
474
+ if (args.liveAnthropic) {
475
+ if (providerId !== 'anthropic') {
476
+ throw new Error(
477
+ `live provider mode requires anthropic model ref until additional drivers are implemented; got ${args.model}. Pass --mock to run without a live provider driver.`,
478
+ );
479
+ }
480
+ loadHarnessSecrets({
481
+ cwd: process.cwd(),
482
+ ...(args.secretsFile !== undefined ? { filePath: args.secretsFile } : {}),
483
+ overrideExisting: false,
484
+ });
485
+ const apiKey = process.env.ANTHROPIC_API_KEY;
486
+ if (typeof apiKey !== 'string' || apiKey.trim().length === 0) {
487
+ throw new Error(
488
+ 'ANTHROPIC_API_KEY was not found after loading secrets. Set the key or pass --mock.',
489
+ );
490
+ }
491
+ runtime.registerProviderDriver(
492
+ createAnthropicNimProviderDriver({
493
+ apiKey,
494
+ ...(args.baseUrl !== undefined ? { baseUrl: args.baseUrl } : {}),
495
+ }),
496
+ );
497
+ }
498
+ const providerMode =
499
+ args.liveAnthropic && providerId === 'anthropic'
500
+ ? 'live-anthropic'
501
+ : 'mock-echo (no provider driver registered)';
502
+
503
+ let currentModel: NimModelRef = args.model;
504
+ let uiMode: NimUiMode = args.uiMode;
505
+ let currentSession: SessionHandle;
506
+ let queuedCount = 0;
507
+ let activeRunId: string | undefined;
508
+ let lastEventId: string | undefined;
509
+ if (args.sessionId !== undefined) {
510
+ currentSession = await runtime.resumeSession({
511
+ tenantId: args.tenantId,
512
+ userId: args.userId,
513
+ sessionId: args.sessionId,
514
+ });
515
+ const replay = await runtime.replayEvents({
516
+ tenantId: args.tenantId,
517
+ sessionId: currentSession.sessionId,
518
+ });
519
+ const last = replay.events[replay.events.length - 1];
520
+ lastEventId = last?.event_id;
521
+ queuedCount = queuedCountFromReplayEvents(replay.events);
522
+ } else {
523
+ currentSession = await runtime.startSession({
524
+ tenantId: args.tenantId,
525
+ userId: args.userId,
526
+ model: args.model,
527
+ });
528
+ }
529
+ process.stdout.write(
530
+ `nim tui ready session=${currentSession.sessionId} model=${currentModel} provider=${providerMode}\n`,
531
+ );
532
+ process.stdout.write(`queue depth ${String(queuedCount)}\n`);
533
+ if (!args.liveAnthropic) {
534
+ process.stdout.write('nim tui note: running deterministic mock mode via --mock.\n');
535
+ }
536
+ printHelp();
537
+
538
+ const rl = createInterface({
539
+ input: process.stdin,
540
+ output: process.stdout,
541
+ terminal: true,
542
+ });
543
+
544
+ try {
545
+ while (true) {
546
+ const line = await rl.question('nim> ');
547
+ if (line.trim().length === 0) {
548
+ continue;
549
+ }
550
+ let command: Command;
551
+ try {
552
+ command = parseNimTuiCommand(line);
553
+ } catch (error) {
554
+ process.stdout.write(`${error instanceof Error ? error.message : String(error)}\n`);
555
+ continue;
556
+ }
557
+
558
+ if (command.type === 'help') {
559
+ printHelp();
560
+ continue;
561
+ }
562
+ if (command.type === 'exit') {
563
+ break;
564
+ }
565
+ if (command.type === 'state') {
566
+ process.stdout.write(
567
+ JSON.stringify(
568
+ {
569
+ tenantId: currentSession.tenantId,
570
+ userId: currentSession.userId,
571
+ sessionId: currentSession.sessionId,
572
+ model: currentModel,
573
+ uiMode: uiModeLabel(uiMode),
574
+ activeRunId: activeRunId ?? null,
575
+ queuedCount,
576
+ lastEventId: lastEventId ?? null,
577
+ },
578
+ null,
579
+ 2,
580
+ ) + '\n',
581
+ );
582
+ continue;
583
+ }
584
+ if (command.type === 'mode') {
585
+ uiMode = command.mode;
586
+ process.stdout.write(`ui mode set to ${uiModeLabel(uiMode)}\n`);
587
+ continue;
588
+ }
589
+ if (command.type === 'switch-model') {
590
+ await runtime.switchModel({
591
+ sessionId: currentSession.sessionId,
592
+ model: command.model,
593
+ reason: 'manual',
594
+ });
595
+ currentModel = command.model;
596
+ process.stdout.write(`switched model to ${currentModel}\n`);
597
+ continue;
598
+ }
599
+ if (command.type === 'session-new') {
600
+ currentSession = await runtime.startSession({
601
+ tenantId: args.tenantId,
602
+ userId: args.userId,
603
+ model: currentModel,
604
+ });
605
+ activeRunId = undefined;
606
+ lastEventId = undefined;
607
+ queuedCount = 0;
608
+ process.stdout.write(`new session ${currentSession.sessionId}\n`);
609
+ process.stdout.write(`queue depth ${String(queuedCount)}\n`);
610
+ continue;
611
+ }
612
+ if (command.type === 'session-resume') {
613
+ currentSession = await runtime.resumeSession({
614
+ tenantId: args.tenantId,
615
+ userId: args.userId,
616
+ sessionId: command.sessionId,
617
+ });
618
+ activeRunId = undefined;
619
+ const replay = await runtime.replayEvents({
620
+ tenantId: args.tenantId,
621
+ sessionId: currentSession.sessionId,
622
+ });
623
+ const last = replay.events[replay.events.length - 1];
624
+ lastEventId = last?.event_id;
625
+ queuedCount = queuedCountFromReplayEvents(replay.events);
626
+ process.stdout.write(`resumed session ${currentSession.sessionId}\n`);
627
+ process.stdout.write(`queue depth ${String(queuedCount)}\n`);
628
+ continue;
629
+ }
630
+ if (command.type === 'abort') {
631
+ if (activeRunId === undefined) {
632
+ process.stdout.write('no active run\n');
633
+ continue;
634
+ }
635
+ await runtime.abortTurn({
636
+ runId: activeRunId,
637
+ reason: 'manual',
638
+ });
639
+ process.stdout.write(`abort requested for ${activeRunId}\n`);
640
+ continue;
641
+ }
642
+ if (command.type === 'queue') {
643
+ const queued = await runtime.queueTurn({
644
+ sessionId: currentSession.sessionId,
645
+ text: command.text,
646
+ priority: command.priority,
647
+ });
648
+ if (queued.queued) {
649
+ if (
650
+ typeof queued.position === 'number' &&
651
+ Number.isInteger(queued.position) &&
652
+ queued.position >= 0
653
+ ) {
654
+ queuedCount = Math.max(queuedCount, queued.position + 1);
655
+ } else {
656
+ queuedCount += 1;
657
+ }
658
+ }
659
+ process.stdout.write(`${JSON.stringify(queued)}\n`);
660
+ process.stdout.write(`queue depth ${String(queuedCount)}\n`);
661
+ continue;
662
+ }
663
+ if (command.type === 'steer') {
664
+ const steered = await runtime.steerTurn({
665
+ sessionId: currentSession.sessionId,
666
+ ...(activeRunId !== undefined ? { runId: activeRunId } : {}),
667
+ text: command.text,
668
+ });
669
+ process.stdout.write(`${JSON.stringify(steered)}\n`);
670
+ continue;
671
+ }
672
+ if (command.type === 'replay') {
673
+ const replay = await runtime.replayEvents({
674
+ tenantId: args.tenantId,
675
+ sessionId: currentSession.sessionId,
676
+ includeThoughtDeltas: true,
677
+ includeToolArgumentDeltas: true,
678
+ });
679
+ const tail = replay.events.slice(-command.count);
680
+ for (const event of tail) {
681
+ process.stdout.write(
682
+ `[replay ${String(event.event_seq)}] ${event.type} run=${event.run_id} source=${event.source}\n`,
683
+ );
684
+ }
685
+ const last = tail[tail.length - 1];
686
+ if (last !== undefined) {
687
+ lastEventId = last.event_id;
688
+ }
689
+ continue;
690
+ }
691
+ if (command.type === 'send') {
692
+ const turn = await runtime.sendTurn({
693
+ sessionId: currentSession.sessionId,
694
+ input: command.text,
695
+ idempotencyKey: `nim-cli:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`,
696
+ });
697
+ activeRunId = turn.runId;
698
+ process.stdout.write(`run started ${turn.runId}\n`);
699
+ const [trace, turnResult] = await Promise.all([
700
+ collectTurnTrace({
701
+ runtime,
702
+ tenantId: args.tenantId,
703
+ sessionId: currentSession.sessionId,
704
+ runId: turn.runId,
705
+ ...(lastEventId !== undefined ? { fromEventIdExclusive: lastEventId } : {}),
706
+ uiMode,
707
+ }),
708
+ turn.done,
709
+ ]);
710
+ lastEventId = trace.lastEventId ?? lastEventId;
711
+ activeRunId = undefined;
712
+ process.stdout.write(`run completed ${turnResult.terminalState}\n`);
713
+ if (queuedCount > 0) {
714
+ queuedCount = Math.max(0, queuedCount - 1);
715
+ process.stdout.write(`queue depth ${String(queuedCount)}\n`);
716
+ }
717
+ if (trace.frameLines.length > 0) {
718
+ process.stdout.write('frame:\n');
719
+ for (const lineItem of trace.frameLines) {
720
+ process.stdout.write(` ${lineItem}\n`);
721
+ }
722
+ }
723
+ }
724
+ }
725
+ } finally {
726
+ rl.close();
727
+ runtimeHandle.close();
728
+ }
729
+ }
730
+
731
+ export async function runNimTuiSmoke(
732
+ argv: readonly string[],
733
+ options: RunNimTuiSmokeOptions = {},
734
+ ): Promise<number> {
735
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
736
+ printUsage();
737
+ return 0;
738
+ }
739
+ const args = parseNimTuiArgs(argv, {
740
+ sessionName: options.sessionName ?? null,
741
+ });
742
+ await runNimTuiInteractive(args);
743
+ return 0;
744
+ }
745
+
746
+ if (import.meta.main) {
747
+ process.exitCode = await runNimTuiSmoke(process.argv.slice(2));
748
+ }