@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,252 @@
1
+ import { measureDisplayWidth } from './text-layout.ts';
2
+
3
+ export type UiColor =
4
+ | { kind: 'default' }
5
+ | { kind: 'indexed'; index: number }
6
+ | { kind: 'rgb'; r: number; g: number; b: number };
7
+
8
+ export interface UiStyle {
9
+ readonly fg: UiColor;
10
+ readonly bg: UiColor;
11
+ readonly bold: boolean;
12
+ readonly dim?: boolean;
13
+ readonly italic?: boolean;
14
+ readonly underline?: boolean;
15
+ readonly inverse?: boolean;
16
+ }
17
+
18
+ export interface UiCell {
19
+ glyph: string;
20
+ continued: boolean;
21
+ style: UiStyle;
22
+ }
23
+
24
+ const DEFAULT_COLOR: UiColor = {
25
+ kind: 'default',
26
+ };
27
+
28
+ export const DEFAULT_UI_STYLE: UiStyle = {
29
+ fg: DEFAULT_COLOR,
30
+ bg: DEFAULT_COLOR,
31
+ bold: false,
32
+ };
33
+
34
+ function cloneColor(color: UiColor): UiColor {
35
+ if (color.kind === 'default') {
36
+ return DEFAULT_COLOR;
37
+ }
38
+ if (color.kind === 'indexed') {
39
+ return {
40
+ kind: 'indexed',
41
+ index: color.index,
42
+ };
43
+ }
44
+ return {
45
+ kind: 'rgb',
46
+ r: color.r,
47
+ g: color.g,
48
+ b: color.b,
49
+ };
50
+ }
51
+
52
+ function cloneStyle(style: UiStyle): UiStyle {
53
+ return {
54
+ fg: cloneColor(style.fg),
55
+ bg: cloneColor(style.bg),
56
+ bold: style.bold,
57
+ ...(style.dim === true ? { dim: true } : {}),
58
+ ...(style.italic === true ? { italic: true } : {}),
59
+ ...(style.underline === true ? { underline: true } : {}),
60
+ ...(style.inverse === true ? { inverse: true } : {}),
61
+ };
62
+ }
63
+
64
+ function styleFlag(value: boolean | undefined): boolean {
65
+ return value === true;
66
+ }
67
+
68
+ function styleEqual(left: UiStyle, right: UiStyle): boolean {
69
+ if (left.bold !== right.bold) {
70
+ return false;
71
+ }
72
+ if (styleFlag(left.dim) !== styleFlag(right.dim)) {
73
+ return false;
74
+ }
75
+ if (styleFlag(left.italic) !== styleFlag(right.italic)) {
76
+ return false;
77
+ }
78
+ if (styleFlag(left.underline) !== styleFlag(right.underline)) {
79
+ return false;
80
+ }
81
+ if (styleFlag(left.inverse) !== styleFlag(right.inverse)) {
82
+ return false;
83
+ }
84
+ if (left.fg.kind !== right.fg.kind || left.bg.kind !== right.bg.kind) {
85
+ return false;
86
+ }
87
+
88
+ if (left.fg.kind === 'indexed') {
89
+ if (left.fg.index !== (right.fg as Extract<UiColor, { kind: 'indexed' }>).index) {
90
+ return false;
91
+ }
92
+ } else if (left.fg.kind === 'rgb') {
93
+ const typedRight = right.fg as Extract<UiColor, { kind: 'rgb' }>;
94
+ if (left.fg.r !== typedRight.r || left.fg.g !== typedRight.g || left.fg.b !== typedRight.b) {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ if (left.bg.kind === 'indexed') {
100
+ if (left.bg.index !== (right.bg as Extract<UiColor, { kind: 'indexed' }>).index) {
101
+ return false;
102
+ }
103
+ } else if (left.bg.kind === 'rgb') {
104
+ const typedRight = right.bg as Extract<UiColor, { kind: 'rgb' }>;
105
+ if (left.bg.r !== typedRight.r || left.bg.g !== typedRight.g || left.bg.b !== typedRight.b) {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ return true;
111
+ }
112
+
113
+ function colorSgrCodes(color: UiColor, target: 'fg' | 'bg'): readonly string[] {
114
+ const prefix = target === 'fg' ? '38' : '48';
115
+ if (color.kind === 'default') {
116
+ return [target === 'fg' ? '39' : '49'];
117
+ }
118
+ if (color.kind === 'indexed') {
119
+ return [prefix, '5', String(color.index)];
120
+ }
121
+ return [prefix, '2', String(color.r), String(color.g), String(color.b)];
122
+ }
123
+
124
+ function styleToSgr(style: UiStyle): string {
125
+ const codes: string[] = ['0'];
126
+ if (style.bold) {
127
+ codes.push('1');
128
+ }
129
+ if (styleFlag(style.dim)) {
130
+ codes.push('2');
131
+ }
132
+ if (styleFlag(style.italic)) {
133
+ codes.push('3');
134
+ }
135
+ if (styleFlag(style.underline)) {
136
+ codes.push('4');
137
+ }
138
+ if (styleFlag(style.inverse)) {
139
+ codes.push('7');
140
+ }
141
+ codes.push(...colorSgrCodes(style.fg, 'fg'));
142
+ codes.push(...colorSgrCodes(style.bg, 'bg'));
143
+ return `\u001b[${codes.join(';')}m`;
144
+ }
145
+
146
+ export class SurfaceBuffer {
147
+ public readonly cols: number;
148
+ public readonly rows: number;
149
+ public readonly baseStyle: UiStyle;
150
+ public readonly cells: UiCell[];
151
+
152
+ constructor(cols: number, rows: number, baseStyle: UiStyle = DEFAULT_UI_STYLE) {
153
+ this.cols = Math.max(1, cols);
154
+ this.rows = Math.max(1, rows);
155
+ this.baseStyle = cloneStyle(baseStyle);
156
+ this.cells = Array.from({ length: this.cols * this.rows }, () => ({
157
+ glyph: ' ',
158
+ continued: false,
159
+ style: cloneStyle(this.baseStyle),
160
+ }));
161
+ }
162
+
163
+ private cellOffset(col: number, row: number): number {
164
+ return row * this.cols + col;
165
+ }
166
+
167
+ public fillRow(row: number, style: UiStyle): void {
168
+ if (row < 0 || row >= this.rows) {
169
+ return;
170
+ }
171
+ const typedStyle = cloneStyle(style);
172
+ for (let col = 0; col < this.cols; col += 1) {
173
+ const cell = this.cells[this.cellOffset(col, row)]!;
174
+ cell.glyph = ' ';
175
+ cell.continued = false;
176
+ cell.style = typedStyle;
177
+ }
178
+ }
179
+
180
+ public drawText(
181
+ colStart: number,
182
+ row: number,
183
+ text: string,
184
+ style: UiStyle = this.baseStyle,
185
+ ): void {
186
+ if (row < 0 || row >= this.rows || colStart >= this.cols) {
187
+ return;
188
+ }
189
+
190
+ let col = Math.max(0, colStart);
191
+ let lastGlyphCol: number | null = null;
192
+ const typedStyle = cloneStyle(style);
193
+ for (const glyph of text) {
194
+ const width = Math.max(0, measureDisplayWidth(glyph));
195
+ if (width === 0) {
196
+ if (lastGlyphCol !== null) {
197
+ const cell = this.cells[this.cellOffset(lastGlyphCol, row)]!;
198
+ cell.glyph += glyph;
199
+ }
200
+ continue;
201
+ }
202
+ if (col >= this.cols) {
203
+ break;
204
+ }
205
+
206
+ if (width === 1) {
207
+ const cell = this.cells[this.cellOffset(col, row)]!;
208
+ cell.glyph = glyph;
209
+ cell.continued = false;
210
+ cell.style = typedStyle;
211
+ lastGlyphCol = col;
212
+ col += 1;
213
+ continue;
214
+ }
215
+
216
+ if (col + width > this.cols) {
217
+ break;
218
+ }
219
+ const first = this.cells[this.cellOffset(col, row)]!;
220
+ first.glyph = glyph;
221
+ first.continued = false;
222
+ first.style = typedStyle;
223
+ for (let offset = 1; offset < width && col + offset < this.cols; offset += 1) {
224
+ const cell = this.cells[this.cellOffset(col + offset, row)]!;
225
+ cell.glyph = '';
226
+ cell.continued = true;
227
+ cell.style = typedStyle;
228
+ }
229
+ lastGlyphCol = col;
230
+ col += width;
231
+ }
232
+ }
233
+
234
+ public renderAnsiRows(): readonly string[] {
235
+ const rows: string[] = [];
236
+ for (let row = 0; row < this.rows; row += 1) {
237
+ let output = '';
238
+ let lastStyle: UiStyle | null = null;
239
+ for (let col = 0; col < this.cols; col += 1) {
240
+ const cell = this.cells[this.cellOffset(col, row)]!;
241
+ if (lastStyle === null || !styleEqual(lastStyle, cell.style)) {
242
+ output += styleToSgr(cell.style);
243
+ lastStyle = cell.style;
244
+ }
245
+ output += cell.continued ? '' : cell.glyph.length > 0 ? cell.glyph : ' ';
246
+ }
247
+ output += '\u001b[0m';
248
+ rows.push(output);
249
+ }
250
+ return rows;
251
+ }
252
+ }
@@ -0,0 +1,210 @@
1
+ function isCombiningMark(codePoint: number): boolean {
2
+ if (codePoint < 0x0300) return false;
3
+ if (codePoint <= 0x036f) return true; // Combining Diacritical Marks
4
+ if (codePoint < 0x0483) return false;
5
+ if (codePoint <= 0x0489) return true; // Cyrillic combining marks
6
+ if (codePoint < 0x0591) return false;
7
+ if (codePoint <= 0x05bd) return true; // Hebrew combining marks
8
+ if (codePoint === 0x05bf) return true;
9
+ if (codePoint === 0x05c1 || codePoint === 0x05c2) return true;
10
+ if (codePoint === 0x05c4 || codePoint === 0x05c5) return true;
11
+ if (codePoint === 0x05c7) return true;
12
+ if (codePoint < 0x0610) return false;
13
+ if (codePoint <= 0x061a) return true; // Arabic combining marks
14
+ if (codePoint < 0x064b) return false;
15
+ if (codePoint <= 0x065f) return true; // Arabic combining marks (extended)
16
+ if (codePoint === 0x0670) return true;
17
+ if (codePoint < 0x06d6) return false;
18
+ if (codePoint <= 0x06dc) return true;
19
+ if (codePoint < 0x06df) return false;
20
+ if (codePoint <= 0x06e4) return true;
21
+ if (codePoint === 0x06e7 || codePoint === 0x06e8) return true;
22
+ if (codePoint === 0x06ea || codePoint === 0x06eb || codePoint === 0x06ec || codePoint === 0x06ed)
23
+ return true;
24
+ if (codePoint < 0x0730) return false;
25
+ if (codePoint <= 0x074a) return true; // Syriac combining marks
26
+ if (codePoint < 0x0900) return false;
27
+ if (codePoint <= 0x0903) return true; // Devanagari
28
+ if (codePoint < 0x093a) return false;
29
+ if (codePoint <= 0x094f) return true;
30
+ if (codePoint < 0x0951) return false;
31
+ if (codePoint <= 0x0957) return true;
32
+ if (codePoint < 0x0e31) return false;
33
+ if (codePoint === 0x0e31) return true; // Thai
34
+ if (codePoint >= 0x0e34 && codePoint <= 0x0e3a) return true;
35
+ if (codePoint >= 0x0e47 && codePoint <= 0x0e4e) return true;
36
+ if (codePoint < 0x1ab0) return false;
37
+ if (codePoint <= 0x1aff) return true; // Combining Diacritical Marks Extended
38
+ if (codePoint < 0x1dc0) return false;
39
+ if (codePoint <= 0x1dff) return true; // Combining Diacritical Marks Supplement
40
+ if (codePoint < 0x20d0) return false;
41
+ if (codePoint <= 0x20ff) return true; // Combining Diacritical Marks for Symbols
42
+ if (codePoint < 0xfe00) return false;
43
+ if (codePoint <= 0xfe0f) return true; // Variation Selectors
44
+ if (codePoint < 0xfe20) return false;
45
+ if (codePoint <= 0xfe2f) return true; // Combining Half Marks
46
+ if (codePoint < 0xe0100) return false;
47
+ if (codePoint <= 0xe01ef) return true; // Variation Selectors Supplement
48
+ return false;
49
+ }
50
+
51
+ const WIDE_RANGES: ReadonlyArray<readonly [number, number]> = [
52
+ [0x1100, 0x115f],
53
+ [0x2329, 0x232a],
54
+ [0x2e80, 0xa4cf],
55
+ [0xac00, 0xd7a3],
56
+ [0xf900, 0xfaff],
57
+ [0xfe10, 0xfe19],
58
+ [0xfe30, 0xfe6f],
59
+ [0xff00, 0xff60],
60
+ [0xffe0, 0xffe6],
61
+ [0x1f300, 0x1faff],
62
+ ];
63
+
64
+ function isWideCodePoint(codePoint: number): boolean {
65
+ for (const [start, end] of WIDE_RANGES) {
66
+ if (codePoint >= start && codePoint <= end) {
67
+ return true;
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+
73
+ export function measureDisplayWidth(text: string): number {
74
+ let width = 0;
75
+ for (const char of text) {
76
+ const codePoint = char.codePointAt(0)!;
77
+ if (codePoint < 0x20 || (codePoint >= 0x7f && codePoint < 0xa0)) {
78
+ continue;
79
+ }
80
+ if (isCombiningMark(codePoint)) {
81
+ continue;
82
+ }
83
+ width += isWideCodePoint(codePoint) ? 2 : 1;
84
+ }
85
+ return width;
86
+ }
87
+
88
+ export function wrapTextForColumns(text: string, cols: number): string[] {
89
+ if (cols <= 0) {
90
+ return [''];
91
+ }
92
+
93
+ const lines: string[] = [];
94
+ let current = '';
95
+ let currentWidth = 0;
96
+ for (const char of text) {
97
+ if (char === '\n') {
98
+ lines.push(current);
99
+ current = '';
100
+ currentWidth = 0;
101
+ continue;
102
+ }
103
+ const charWidth = Math.max(1, measureDisplayWidth(char));
104
+ if (currentWidth + charWidth > cols) {
105
+ lines.push(current);
106
+ current = '';
107
+ currentWidth = 0;
108
+ }
109
+ current += char;
110
+ currentWidth += charWidth;
111
+ }
112
+ lines.push(current);
113
+ return lines;
114
+ }
115
+
116
+ export interface WrappingInputBuffer {
117
+ readonly text: string;
118
+ readonly cursor: number;
119
+ }
120
+
121
+ export interface RenderWrappingInputLinesOptions {
122
+ readonly buffer: WrappingInputBuffer;
123
+ readonly width: number;
124
+ readonly cursorVisible?: boolean;
125
+ readonly cursorToken?: string;
126
+ readonly linePrefix?: string;
127
+ }
128
+
129
+ function normalizedCursor(buffer: WrappingInputBuffer): number {
130
+ if (!Number.isFinite(buffer.cursor)) {
131
+ return buffer.text.length;
132
+ }
133
+ return Math.max(0, Math.min(buffer.text.length, Math.floor(buffer.cursor)));
134
+ }
135
+
136
+ function consumedGlyphsText(consumed: ReadonlyArray<{ glyph: string; width: number }>): string {
137
+ let output = '';
138
+ for (const entry of consumed) {
139
+ output += entry.glyph;
140
+ }
141
+ return output;
142
+ }
143
+
144
+ export class TextLayoutEngine {
145
+ public constructor() {}
146
+
147
+ public measure(text: string): number {
148
+ return measureDisplayWidth(text);
149
+ }
150
+
151
+ public wrap(text: string, cols: number): readonly string[] {
152
+ return wrapTextForColumns(text, cols);
153
+ }
154
+
155
+ public truncate(text: string, width: number): string {
156
+ const safeWidth = Math.max(0, Math.floor(width));
157
+ if (safeWidth === 0 || text.length === 0) {
158
+ return '';
159
+ }
160
+
161
+ const consumed: Array<{ glyph: string; width: number }> = [];
162
+ let consumedWidth = 0;
163
+ let truncated = false;
164
+ for (const glyph of text) {
165
+ const glyphWidth = Math.max(1, this.measure(glyph));
166
+ if (consumedWidth + glyphWidth > safeWidth) {
167
+ truncated = true;
168
+ break;
169
+ }
170
+ consumed.push({ glyph, width: glyphWidth });
171
+ consumedWidth += glyphWidth;
172
+ }
173
+
174
+ if (!truncated) {
175
+ return consumedGlyphsText(consumed);
176
+ }
177
+ if (safeWidth === 1) {
178
+ return '…';
179
+ }
180
+ while (consumed.length > 0 && consumedWidth + 1 > safeWidth) {
181
+ const removed = consumed.pop()!;
182
+ consumedWidth -= removed.width;
183
+ }
184
+ return `${consumedGlyphsText(consumed)}…`;
185
+ }
186
+ }
187
+
188
+ export class WrappingInputRenderer {
189
+ constructor(private readonly layout: TextLayoutEngine = new TextLayoutEngine()) {}
190
+
191
+ public renderLines(options: RenderWrappingInputLinesOptions): readonly string[] {
192
+ const cursor = normalizedCursor(options.buffer);
193
+ const cursorToken = options.cursorToken ?? '█';
194
+ const linePrefix = options.linePrefix ?? '';
195
+ const cursorVisible = options.cursorVisible ?? true;
196
+ const textWithCursor = !cursorVisible
197
+ ? options.buffer.text
198
+ : cursor >= options.buffer.text.length
199
+ ? `${options.buffer.text}${cursorToken}`
200
+ : options.buffer.text.slice(0, cursor) +
201
+ cursorToken +
202
+ options.buffer.text.slice(cursor + 1);
203
+ const logicalLines = textWithCursor.split('\n');
204
+ const wrapped: string[] = [];
205
+ for (const line of logicalLines) {
206
+ wrapped.push(...this.layout.wrap(`${linePrefix}${line}`, options.width));
207
+ }
208
+ return wrapped.length === 0 ? [''] : wrapped;
209
+ }
210
+ }
@@ -0,0 +1,239 @@
1
+ import type { NimEventEnvelope } from './events.ts';
2
+
3
+ export type NimProviderId = string;
4
+ export type NimModelRef = `${string}/${string}`;
5
+
6
+ export type NimToolPolicy = {
7
+ readonly hash: string;
8
+ readonly allow: readonly string[];
9
+ readonly deny: readonly string[];
10
+ };
11
+
12
+ export type NimProvider = {
13
+ readonly id: NimProviderId;
14
+ readonly displayName: string;
15
+ readonly models: readonly NimModelRef[];
16
+ };
17
+
18
+ export type NimToolDefinition = {
19
+ readonly name: string;
20
+ readonly description: string;
21
+ };
22
+
23
+ export type SessionHandle = {
24
+ readonly sessionId: string;
25
+ readonly tenantId: string;
26
+ readonly userId: string;
27
+ readonly model: NimModelRef;
28
+ readonly lane: string;
29
+ readonly soulHash?: string;
30
+ readonly skillsSnapshotVersion?: number;
31
+ };
32
+
33
+ export type TurnResult = {
34
+ readonly runId: string;
35
+ readonly terminalState: 'completed' | 'failed' | 'aborted';
36
+ };
37
+
38
+ export type TurnHandle = {
39
+ readonly runId: string;
40
+ readonly sessionId: string;
41
+ readonly idempotencyKey: string;
42
+ readonly done: Promise<TurnResult>;
43
+ };
44
+
45
+ export type StartSessionInput = {
46
+ readonly tenantId: string;
47
+ readonly userId: string;
48
+ readonly model: NimModelRef;
49
+ readonly lane?: string;
50
+ };
51
+
52
+ export type ResumeSessionInput = {
53
+ readonly tenantId: string;
54
+ readonly userId: string;
55
+ readonly sessionId: string;
56
+ };
57
+
58
+ export type ListSessionsInput = {
59
+ readonly tenantId: string;
60
+ readonly userId: string;
61
+ };
62
+
63
+ export type ListSessionsResult = {
64
+ readonly sessions: readonly SessionHandle[];
65
+ };
66
+
67
+ export type SwitchModelInput = {
68
+ readonly sessionId: string;
69
+ readonly model: NimModelRef;
70
+ readonly reason: 'manual' | 'policy' | 'fallback';
71
+ };
72
+
73
+ export type SendTurnInput = {
74
+ readonly sessionId: string;
75
+ readonly input: string;
76
+ readonly idempotencyKey: string;
77
+ readonly lane?: string;
78
+ readonly abortSignal?: AbortSignal;
79
+ };
80
+
81
+ export type AbortTurnInput = {
82
+ readonly runId: string;
83
+ readonly reason?: 'manual' | 'timeout' | 'policy';
84
+ };
85
+
86
+ export type CompactSessionInput = {
87
+ readonly sessionId: string;
88
+ readonly trigger: 'manual' | 'overflow' | 'policy';
89
+ readonly includeMemoryFlush?: boolean;
90
+ };
91
+
92
+ export type CompactionResult = {
93
+ readonly compacted: boolean;
94
+ readonly summaryEventId?: string;
95
+ readonly reason?: string;
96
+ };
97
+
98
+ export type SteerTurnInput = {
99
+ readonly sessionId: string;
100
+ readonly runId?: string;
101
+ readonly text: string;
102
+ };
103
+
104
+ export type SteerTurnResult = {
105
+ readonly accepted: boolean;
106
+ readonly reason?: 'no-active-run' | 'not-streaming' | 'compacting' | 'rate-limited';
107
+ };
108
+
109
+ export type QueueTurnInput = {
110
+ readonly sessionId: string;
111
+ readonly text: string;
112
+ readonly priority?: 'normal' | 'high';
113
+ readonly dedupeKey?: string;
114
+ };
115
+
116
+ export type QueueTurnResult = {
117
+ readonly queued: boolean;
118
+ readonly queueId?: string;
119
+ readonly position?: number;
120
+ readonly reason?: 'duplicate' | 'queue-full' | 'invalid-state';
121
+ };
122
+
123
+ export type StreamEventsInput = {
124
+ readonly tenantId: string;
125
+ readonly sessionId?: string;
126
+ readonly runId?: string;
127
+ readonly fromEventIdExclusive?: string;
128
+ readonly fidelity?: 'raw' | 'semantic';
129
+ readonly includeThoughtDeltas?: boolean;
130
+ readonly includeToolArgumentDeltas?: boolean;
131
+ };
132
+
133
+ export type StreamUiInput = {
134
+ readonly tenantId: string;
135
+ readonly sessionId?: string;
136
+ readonly runId?: string;
137
+ readonly mode: 'debug' | 'seamless';
138
+ };
139
+
140
+ export type ReplayEventsInput = {
141
+ readonly tenantId: string;
142
+ readonly sessionId?: string;
143
+ readonly runId?: string;
144
+ readonly fromEventIdExclusive?: string;
145
+ readonly toEventIdInclusive?: string;
146
+ readonly fidelity?: 'raw' | 'semantic';
147
+ readonly includeThoughtDeltas?: boolean;
148
+ readonly includeToolArgumentDeltas?: boolean;
149
+ };
150
+
151
+ export type ReplayEventsResult = {
152
+ readonly events: readonly NimEventEnvelope[];
153
+ };
154
+
155
+ export type NimTelemetrySink = {
156
+ readonly name: string;
157
+ record(event: NimEventEnvelope): void;
158
+ };
159
+
160
+ export type SoulSource = {
161
+ readonly name: string;
162
+ };
163
+
164
+ export type SkillSource = {
165
+ readonly name: string;
166
+ };
167
+
168
+ export type MemoryStore = {
169
+ readonly name: string;
170
+ };
171
+
172
+ export type SoulSnapshot = {
173
+ readonly hash: string;
174
+ };
175
+
176
+ export type SkillsSnapshot = {
177
+ readonly hash: string;
178
+ readonly version: number;
179
+ };
180
+
181
+ export type MemorySnapshot = {
182
+ readonly hash: string;
183
+ };
184
+
185
+ export type NimUiEvent =
186
+ | {
187
+ readonly type: 'assistant.state';
188
+ readonly state: 'thinking' | 'tool-calling' | 'responding' | 'idle';
189
+ }
190
+ | {
191
+ readonly type: 'assistant.text.delta';
192
+ readonly text: string;
193
+ }
194
+ | {
195
+ readonly type: 'assistant.text.message';
196
+ readonly text: string;
197
+ }
198
+ | {
199
+ readonly type: 'tool.activity';
200
+ readonly toolCallId: string;
201
+ readonly toolName: string;
202
+ readonly phase: 'start' | 'end' | 'error';
203
+ }
204
+ | {
205
+ readonly type: 'system.notice';
206
+ readonly text: string;
207
+ };
208
+
209
+ export interface NimRuntime {
210
+ startSession(input: StartSessionInput): Promise<SessionHandle>;
211
+ resumeSession(input: ResumeSessionInput): Promise<SessionHandle>;
212
+ listSessions(input: ListSessionsInput): Promise<ListSessionsResult>;
213
+
214
+ registerTools(tools: readonly NimToolDefinition[]): void;
215
+ setToolPolicy(policy: NimToolPolicy): void;
216
+
217
+ registerProvider(provider: NimProvider): void;
218
+ switchModel(input: SwitchModelInput): Promise<void>;
219
+ registerTelemetrySink(sink: NimTelemetrySink): void;
220
+
221
+ registerSoulSource(source: SoulSource): void;
222
+ registerSkillSource(source: SkillSource): void;
223
+ registerMemoryStore(store: MemoryStore): void;
224
+
225
+ loadSoul(): Promise<SoulSnapshot>;
226
+ loadSkills(): Promise<SkillsSnapshot>;
227
+ loadMemory(): Promise<MemorySnapshot>;
228
+
229
+ sendTurn(input: SendTurnInput): Promise<TurnHandle>;
230
+ abortTurn(input: AbortTurnInput): Promise<void>;
231
+ steerTurn(input: SteerTurnInput): Promise<SteerTurnResult>;
232
+ queueTurn(input: QueueTurnInput): Promise<QueueTurnResult>;
233
+
234
+ compactSession(input: CompactSessionInput): Promise<CompactionResult>;
235
+
236
+ streamEvents(input: StreamEventsInput): AsyncIterable<NimEventEnvelope>;
237
+ streamUi(input: StreamUiInput): AsyncIterable<NimUiEvent>;
238
+ replayEvents(input: ReplayEventsInput): Promise<ReplayEventsResult>;
239
+ }