@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
@@ -0,0 +1,94 @@
1
+ import type { DiffUiFinderResult, DiffUiModel } from './types.ts';
2
+
3
+ function filePathFromModel(model: DiffUiModel, fileIndex: number): string {
4
+ const file = model.diff.files[fileIndex]!;
5
+ return file.newPath ?? file.oldPath ?? file.fileId;
6
+ }
7
+
8
+ export function scoreFinderPath(query: string, path: string): number {
9
+ const trimmedQuery = query.trim().toLowerCase();
10
+ if (trimmedQuery.length === 0) {
11
+ return 0;
12
+ }
13
+
14
+ const candidate = path.toLowerCase();
15
+ let queryIndex = 0;
16
+ let score = 0;
17
+ let streak = 0;
18
+ let firstMatchIndex = -1;
19
+
20
+ for (let candidateIndex = 0; candidateIndex < candidate.length; candidateIndex += 1) {
21
+ if (queryIndex >= trimmedQuery.length) {
22
+ break;
23
+ }
24
+
25
+ const current = candidate[candidateIndex]!;
26
+ if (current !== trimmedQuery[queryIndex]) {
27
+ streak = 0;
28
+ continue;
29
+ }
30
+
31
+ if (firstMatchIndex < 0) {
32
+ firstMatchIndex = candidateIndex;
33
+ }
34
+ streak += 1;
35
+ score += 10 + streak * 4;
36
+
37
+ const prev = candidateIndex > 0 ? candidate[candidateIndex - 1] : '/';
38
+ if (prev === '/' || prev === '-' || prev === '_' || prev === '.') {
39
+ score += 6;
40
+ }
41
+
42
+ queryIndex += 1;
43
+ }
44
+
45
+ if (queryIndex !== trimmedQuery.length) {
46
+ return Number.NEGATIVE_INFINITY;
47
+ }
48
+
49
+ if (firstMatchIndex === 0) {
50
+ score += 12;
51
+ } else if (firstMatchIndex > 0) {
52
+ score += Math.max(0, 8 - firstMatchIndex);
53
+ }
54
+
55
+ score -= Math.floor(candidate.length / 8);
56
+ return score;
57
+ }
58
+
59
+ export function buildFinderResults(
60
+ model: DiffUiModel,
61
+ query: string,
62
+ maxResults = 200,
63
+ ): readonly DiffUiFinderResult[] {
64
+ const trimmedQuery = query.trim();
65
+ const results: DiffUiFinderResult[] = [];
66
+
67
+ for (let fileIndex = 0; fileIndex < model.diff.files.length; fileIndex += 1) {
68
+ const file = model.diff.files[fileIndex]!;
69
+ const path = filePathFromModel(model, fileIndex);
70
+ const score = trimmedQuery.length === 0 ? 0 : scoreFinderPath(trimmedQuery, path);
71
+ if (score === Number.NEGATIVE_INFINITY) {
72
+ continue;
73
+ }
74
+ results.push({
75
+ fileIndex,
76
+ fileId: file.fileId,
77
+ path,
78
+ score,
79
+ });
80
+ }
81
+
82
+ results.sort((left, right) => {
83
+ if (left.score !== right.score) {
84
+ return right.score - left.score;
85
+ }
86
+ const pathCompare = left.path.localeCompare(right.path);
87
+ if (pathCompare !== 0) {
88
+ return pathCompare;
89
+ }
90
+ return left.fileIndex - right.fileIndex;
91
+ });
92
+
93
+ return results.slice(0, Math.max(1, Math.floor(maxResults)));
94
+ }
@@ -0,0 +1,127 @@
1
+ import type { DiffUiRenderTheme } from './types.ts';
2
+
3
+ type DiffUiSyntaxTokenRole = 'keyword' | 'string' | 'comment' | 'number';
4
+
5
+ interface DiffUiSyntaxToken {
6
+ readonly start: number;
7
+ readonly end: number;
8
+ readonly role: DiffUiSyntaxTokenRole;
9
+ }
10
+
11
+ const JAVASCRIPT_LIKE_LANGUAGES = new Set(['javascript', 'typescript', 'tsx', 'jsx', 'mjs', 'cjs']);
12
+
13
+ const KEYWORD_PATTERN =
14
+ /\b(async|await|break|case|catch|class|const|continue|default|else|export|extends|finally|for|function|if|import|interface|let|new|return|switch|throw|try|type|var|while)\b/gu;
15
+ const NUMBER_PATTERN = /\b\d+(?:\.\d+)?\b/gu;
16
+ const STRING_PATTERN = /'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`/gu;
17
+ const COMMENT_PATTERN = /\/\/.*$/u;
18
+
19
+ function hasOverlap(tokens: readonly DiffUiSyntaxToken[], start: number, end: number): boolean {
20
+ for (const token of tokens) {
21
+ if (start < token.end && end > token.start) {
22
+ return true;
23
+ }
24
+ }
25
+ return false;
26
+ }
27
+
28
+ function collectRegexMatches(
29
+ line: string,
30
+ regex: RegExp,
31
+ role: DiffUiSyntaxTokenRole,
32
+ tokens: DiffUiSyntaxToken[],
33
+ ): void {
34
+ const cloned = new RegExp(regex.source, regex.flags);
35
+ let match = cloned.exec(line);
36
+ while (match !== null) {
37
+ const matched = match[0] ?? '';
38
+ const start = match.index;
39
+ const end = start + matched.length;
40
+ if (matched.length > 0 && !hasOverlap(tokens, start, end)) {
41
+ tokens.push({
42
+ start,
43
+ end,
44
+ role,
45
+ });
46
+ }
47
+ match = cloned.exec(line);
48
+ }
49
+ }
50
+
51
+ function isSyntaxLanguage(language: string | null): boolean {
52
+ if (language === null) {
53
+ return false;
54
+ }
55
+ return JAVASCRIPT_LIKE_LANGUAGES.has(language.toLowerCase());
56
+ }
57
+
58
+ function tokenizeDiffUiSyntaxLine(
59
+ line: string,
60
+ language: string | null,
61
+ ): readonly DiffUiSyntaxToken[] {
62
+ if (!isSyntaxLanguage(language)) {
63
+ return [];
64
+ }
65
+
66
+ const tokens: DiffUiSyntaxToken[] = [];
67
+
68
+ const commentMatch = line.match(COMMENT_PATTERN);
69
+ if (commentMatch !== null && commentMatch.index !== undefined) {
70
+ tokens.push({
71
+ start: commentMatch.index,
72
+ end: line.length,
73
+ role: 'comment',
74
+ });
75
+ }
76
+
77
+ collectRegexMatches(line, STRING_PATTERN, 'string', tokens);
78
+ collectRegexMatches(line, KEYWORD_PATTERN, 'keyword', tokens);
79
+ collectRegexMatches(line, NUMBER_PATTERN, 'number', tokens);
80
+
81
+ tokens.sort((left, right) => left.start - right.start || left.end - right.end);
82
+ return tokens;
83
+ }
84
+
85
+ function ansiForRole(role: DiffUiSyntaxTokenRole, theme: DiffUiRenderTheme): string {
86
+ if (role === 'keyword') {
87
+ return theme.syntaxKeywordAnsi;
88
+ }
89
+ if (role === 'string') {
90
+ return theme.syntaxStringAnsi;
91
+ }
92
+ if (role === 'comment') {
93
+ return theme.syntaxCommentAnsi;
94
+ }
95
+ return theme.syntaxNumberAnsi;
96
+ }
97
+
98
+ export function renderSyntaxAnsiLine(input: {
99
+ readonly line: string;
100
+ readonly language: string | null;
101
+ readonly theme: DiffUiRenderTheme;
102
+ readonly colorEnabled: boolean;
103
+ readonly baseAnsi: string;
104
+ }): string {
105
+ if (!input.colorEnabled) {
106
+ return input.line;
107
+ }
108
+
109
+ const tokens = tokenizeDiffUiSyntaxLine(input.line, input.language);
110
+ if (tokens.length === 0) {
111
+ return input.line;
112
+ }
113
+
114
+ let output = '';
115
+ let cursor = 0;
116
+ for (const token of tokens) {
117
+ if (token.start > cursor) {
118
+ output += input.line.slice(cursor, token.start);
119
+ }
120
+ output += `${ansiForRole(token.role, input.theme)}${input.line.slice(token.start, token.end)}${input.baseAnsi}`;
121
+ cursor = token.end;
122
+ }
123
+ if (cursor < input.line.length) {
124
+ output += input.line.slice(cursor);
125
+ }
126
+ return output;
127
+ }
@@ -0,0 +1,2 @@
1
+ export { diffUiUsage } from './args.ts';
2
+ export { runDiffUiCli } from './runtime.ts';
@@ -0,0 +1,141 @@
1
+ import type { DiffFile, DiffHunk, DiffLine, NormalizedDiff } from '../diff/types.ts';
2
+ import type { DiffUiModel, DiffUiRowKind, DiffUiVirtualRow } from './types.ts';
3
+
4
+ function filePath(file: DiffFile): string {
5
+ return file.newPath ?? file.oldPath ?? file.fileId;
6
+ }
7
+
8
+ function lineKindToRowKind(line: DiffLine): DiffUiRowKind {
9
+ if (line.kind === 'add') {
10
+ return 'code-add';
11
+ }
12
+ if (line.kind === 'del') {
13
+ return 'code-del';
14
+ }
15
+ return 'code-context';
16
+ }
17
+
18
+ function unifiedPrefix(kind: DiffLine['kind']): string {
19
+ if (kind === 'add') {
20
+ return '+';
21
+ }
22
+ if (kind === 'del') {
23
+ return '-';
24
+ }
25
+ return ' ';
26
+ }
27
+
28
+ function buildCodeRow(input: {
29
+ readonly file: DiffFile;
30
+ readonly fileIndex: number;
31
+ readonly hunk: DiffHunk;
32
+ readonly hunkIndex: number;
33
+ readonly line: DiffLine;
34
+ }): DiffUiVirtualRow {
35
+ const rowKind = lineKindToRowKind(input.line);
36
+ const prefix = unifiedPrefix(input.line.kind);
37
+ const unified = `${prefix} ${input.line.text}`;
38
+ const left = input.line.kind === 'add' ? '' : input.line.text;
39
+ const right = input.line.kind === 'del' ? '' : input.line.text;
40
+
41
+ return {
42
+ kind: rowKind,
43
+ unified,
44
+ left,
45
+ right,
46
+ fileId: input.file.fileId,
47
+ hunkId: input.hunk.hunkId,
48
+ fileIndex: input.fileIndex,
49
+ hunkIndex: input.hunkIndex,
50
+ language: input.file.language,
51
+ oldLine: input.line.oldLine,
52
+ newLine: input.line.newLine,
53
+ };
54
+ }
55
+
56
+ export function buildDiffUiModel(diff: NormalizedDiff): DiffUiModel {
57
+ const rows: DiffUiVirtualRow[] = [];
58
+ const fileStartRows: number[] = [];
59
+ const hunkStartRows: number[] = [];
60
+
61
+ let globalHunkIndex = 0;
62
+
63
+ for (let fileIndex = 0; fileIndex < diff.files.length; fileIndex += 1) {
64
+ const file = diff.files[fileIndex]!;
65
+ fileStartRows.push(rows.length);
66
+
67
+ const path = filePath(file);
68
+ rows.push({
69
+ kind: 'file-header',
70
+ unified: `File ${fileIndex + 1}/${diff.files.length}: ${path} (+${file.additions} -${file.deletions})`,
71
+ left: path,
72
+ right: `+${file.additions} -${file.deletions}`,
73
+ fileId: file.fileId,
74
+ hunkId: null,
75
+ fileIndex,
76
+ hunkIndex: null,
77
+ language: file.language,
78
+ oldLine: null,
79
+ newLine: null,
80
+ });
81
+
82
+ for (const hunk of file.hunks) {
83
+ hunkStartRows.push(rows.length);
84
+ const hunkIndex = globalHunkIndex;
85
+ globalHunkIndex += 1;
86
+ rows.push({
87
+ kind: 'hunk-header',
88
+ unified: hunk.header,
89
+ left: hunk.header,
90
+ right: hunk.header,
91
+ fileId: file.fileId,
92
+ hunkId: hunk.hunkId,
93
+ fileIndex,
94
+ hunkIndex,
95
+ language: file.language,
96
+ oldLine: hunk.oldStart,
97
+ newLine: hunk.newStart,
98
+ });
99
+
100
+ for (const line of hunk.lines) {
101
+ rows.push(
102
+ buildCodeRow({
103
+ file,
104
+ fileIndex,
105
+ hunk,
106
+ hunkIndex,
107
+ line,
108
+ }),
109
+ );
110
+ }
111
+ }
112
+ }
113
+
114
+ if (diff.coverage.truncated) {
115
+ rows.push({
116
+ kind: 'notice',
117
+ unified: `coverage truncated: reason=${diff.coverage.reason} skipped=${diff.coverage.skippedFiles} truncated=${diff.coverage.truncatedFiles}`,
118
+ left: '',
119
+ right: '',
120
+ fileId: null,
121
+ hunkId: null,
122
+ fileIndex: null,
123
+ hunkIndex: null,
124
+ language: null,
125
+ oldLine: null,
126
+ newLine: null,
127
+ });
128
+ }
129
+
130
+ return {
131
+ diff,
132
+ rows,
133
+ fileStartRows,
134
+ hunkStartRows,
135
+ };
136
+ }
137
+
138
+ export function maxTopRowForModel(model: DiffUiModel, viewportHeight: number): number {
139
+ const bodyRows = Math.max(1, viewportHeight - 2);
140
+ return Math.max(0, model.rows.length - bodyRows);
141
+ }