@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,136 @@
1
+ import type { DiffBudget, DiffCoverageReason } from './types.ts';
2
+
3
+ interface DiffBudgetUsage {
4
+ readonly files: number;
5
+ readonly hunks: number;
6
+ readonly lines: number;
7
+ readonly bytes: number;
8
+ }
9
+
10
+ interface MutableDiffBudgetUsage {
11
+ files: number;
12
+ hunks: number;
13
+ lines: number;
14
+ bytes: number;
15
+ }
16
+
17
+ interface DiffBudgetCheckpoint {
18
+ readonly allowed: boolean;
19
+ readonly reason: DiffCoverageReason;
20
+ }
21
+
22
+ function takeReason(current: DiffCoverageReason, next: DiffCoverageReason): DiffCoverageReason {
23
+ if (current !== 'none') {
24
+ return current;
25
+ }
26
+ return next;
27
+ }
28
+
29
+ export class DiffBudgetTracker {
30
+ private reason: DiffCoverageReason = 'none';
31
+ private readonly usageMutable: MutableDiffBudgetUsage = {
32
+ files: 0,
33
+ hunks: 0,
34
+ lines: 0,
35
+ bytes: 0,
36
+ };
37
+ private readonly startedAtMs: number;
38
+
39
+ constructor(
40
+ private readonly budget: DiffBudget,
41
+ nowMs = Date.now(),
42
+ ) {
43
+ this.startedAtMs = nowMs;
44
+ }
45
+
46
+ usage(): DiffBudgetUsage {
47
+ return {
48
+ files: this.usageMutable.files,
49
+ hunks: this.usageMutable.hunks,
50
+ lines: this.usageMutable.lines,
51
+ bytes: this.usageMutable.bytes,
52
+ };
53
+ }
54
+
55
+ limitReason(): DiffCoverageReason {
56
+ return this.reason;
57
+ }
58
+
59
+ elapsedMs(nowMs = Date.now()): number {
60
+ return Math.max(0, nowMs - this.startedAtMs);
61
+ }
62
+
63
+ checkRuntime(nowMs = Date.now()): DiffBudgetCheckpoint {
64
+ if (this.elapsedMs(nowMs) >= this.budget.maxRuntimeMs) {
65
+ this.reason = takeReason(this.reason, 'max-runtime-ms');
66
+ return {
67
+ allowed: false,
68
+ reason: this.reason,
69
+ };
70
+ }
71
+ return {
72
+ allowed: true,
73
+ reason: this.reason,
74
+ };
75
+ }
76
+
77
+ addBytes(value: number): DiffBudgetCheckpoint {
78
+ this.usageMutable.bytes += Math.max(0, Math.floor(value));
79
+ if (this.usageMutable.bytes > this.budget.maxBytes) {
80
+ this.reason = takeReason(this.reason, 'max-bytes');
81
+ return {
82
+ allowed: false,
83
+ reason: this.reason,
84
+ };
85
+ }
86
+ return {
87
+ allowed: true,
88
+ reason: this.reason,
89
+ };
90
+ }
91
+
92
+ takeFile(): DiffBudgetCheckpoint {
93
+ if (this.usageMutable.files >= this.budget.maxFiles) {
94
+ this.reason = takeReason(this.reason, 'max-files');
95
+ return {
96
+ allowed: false,
97
+ reason: this.reason,
98
+ };
99
+ }
100
+ this.usageMutable.files += 1;
101
+ return {
102
+ allowed: true,
103
+ reason: this.reason,
104
+ };
105
+ }
106
+
107
+ takeHunk(): DiffBudgetCheckpoint {
108
+ if (this.usageMutable.hunks >= this.budget.maxHunks) {
109
+ this.reason = takeReason(this.reason, 'max-hunks');
110
+ return {
111
+ allowed: false,
112
+ reason: this.reason,
113
+ };
114
+ }
115
+ this.usageMutable.hunks += 1;
116
+ return {
117
+ allowed: true,
118
+ reason: this.reason,
119
+ };
120
+ }
121
+
122
+ takeLine(): DiffBudgetCheckpoint {
123
+ if (this.usageMutable.lines >= this.budget.maxLines) {
124
+ this.reason = takeReason(this.reason, 'max-lines');
125
+ return {
126
+ allowed: false,
127
+ reason: this.reason,
128
+ };
129
+ }
130
+ this.usageMutable.lines += 1;
131
+ return {
132
+ allowed: true,
133
+ reason: this.reason,
134
+ };
135
+ }
136
+ }
@@ -0,0 +1,289 @@
1
+ import { DiffBudgetTracker } from './budget.ts';
2
+ import { computeDiffId } from './hash.ts';
3
+ import {
4
+ buildGitDiffArgs,
5
+ readGitDiffPreflight,
6
+ resolveRangeBaseRef,
7
+ streamGitLines,
8
+ type GitDiffInvocationOptions,
9
+ } from './git-invoke.ts';
10
+ import { GitDiffPatchParser } from './git-parse.ts';
11
+ import type {
12
+ DiffBuildOptions,
13
+ DiffBuildResult,
14
+ DiffBuilder,
15
+ DiffCoverage,
16
+ DiffCoverageReason,
17
+ DiffStreamEvent,
18
+ NormalizedDiff,
19
+ } from './types.ts';
20
+
21
+ interface BuildHooks {
22
+ emit(event: DiffStreamEvent): void;
23
+ }
24
+
25
+ interface NormalizedBuildOptions {
26
+ readonly cwd: string;
27
+ readonly mode: DiffBuildOptions['mode'];
28
+ readonly baseRef: string | null;
29
+ readonly headRef: string | null;
30
+ readonly includeGenerated: boolean;
31
+ readonly includeBinary: boolean;
32
+ readonly noRenames: boolean;
33
+ readonly renameLimit: number | null;
34
+ readonly budget: DiffBuildOptions['budget'];
35
+ }
36
+
37
+ function normalizeOptions(options: DiffBuildOptions): NormalizedBuildOptions {
38
+ return {
39
+ cwd: options.cwd,
40
+ mode: options.mode,
41
+ baseRef: options.baseRef ?? null,
42
+ headRef: options.headRef ?? null,
43
+ includeGenerated: options.includeGenerated ?? false,
44
+ includeBinary: options.includeBinary ?? false,
45
+ noRenames: options.git?.noRenames ?? true,
46
+ renameLimit:
47
+ typeof options.git?.renameLimit === 'number' && Number.isFinite(options.git.renameLimit)
48
+ ? Math.max(0, Math.floor(options.git.renameLimit))
49
+ : null,
50
+ budget: options.budget,
51
+ };
52
+ }
53
+
54
+ function toInvocationOptions(options: NormalizedBuildOptions): GitDiffInvocationOptions {
55
+ return {
56
+ cwd: options.cwd,
57
+ mode: options.mode,
58
+ baseRef: options.baseRef,
59
+ headRef: options.headRef,
60
+ noRenames: options.noRenames,
61
+ renameLimit: options.renameLimit,
62
+ };
63
+ }
64
+
65
+ async function resolveRangeRefs(options: NormalizedBuildOptions): Promise<{
66
+ readonly baseRef: string | null;
67
+ readonly headRef: string | null;
68
+ }> {
69
+ if (options.mode !== 'range') {
70
+ return {
71
+ baseRef: options.baseRef,
72
+ headRef: options.headRef,
73
+ };
74
+ }
75
+
76
+ const headRef = options.headRef ?? 'HEAD';
77
+ if (options.baseRef !== null) {
78
+ return {
79
+ baseRef: options.baseRef,
80
+ headRef,
81
+ };
82
+ }
83
+
84
+ const baseRef = await resolveRangeBaseRef({
85
+ cwd: options.cwd,
86
+ headRef,
87
+ timeoutMs: options.budget.maxRuntimeMs,
88
+ });
89
+ return {
90
+ baseRef,
91
+ headRef,
92
+ };
93
+ }
94
+
95
+ function resolveCoverage(input: {
96
+ filesObservedInPreflight: number;
97
+ filesIncluded: number;
98
+ filesSkippedByFilter: number;
99
+ filesTruncatedByBudget: number;
100
+ limitReason: DiffCoverageReason;
101
+ }): DiffCoverage {
102
+ const remainder = Math.max(
103
+ 0,
104
+ input.filesObservedInPreflight - input.filesIncluded - input.filesSkippedByFilter,
105
+ );
106
+ const truncatedByLimit = input.limitReason !== 'none' ? remainder : 0;
107
+ const truncatedFiles = Math.max(input.filesTruncatedByBudget, truncatedByLimit);
108
+ const skippedFiles = Math.max(
109
+ input.filesSkippedByFilter,
110
+ input.filesObservedInPreflight - input.filesIncluded - truncatedFiles,
111
+ );
112
+ return {
113
+ complete: input.limitReason === 'none' && truncatedFiles === 0,
114
+ truncated: input.limitReason !== 'none' || truncatedFiles > 0,
115
+ skippedFiles,
116
+ truncatedFiles,
117
+ reason: input.limitReason,
118
+ };
119
+ }
120
+
121
+ async function runBuild(
122
+ rawOptions: DiffBuildOptions,
123
+ hooks?: BuildHooks,
124
+ ): Promise<DiffBuildResult> {
125
+ const options = normalizeOptions(rawOptions);
126
+ const startedAtMs = Date.now();
127
+ hooks?.emit({
128
+ type: 'start',
129
+ mode: options.mode,
130
+ });
131
+ const resolvedRefs = await resolveRangeRefs(options);
132
+ const invocation = toInvocationOptions({
133
+ ...options,
134
+ baseRef: resolvedRefs.baseRef,
135
+ headRef: resolvedRefs.headRef,
136
+ });
137
+ const preflight = await readGitDiffPreflight(invocation, options.budget.maxRuntimeMs);
138
+
139
+ const budget = new DiffBudgetTracker(options.budget, startedAtMs);
140
+ const parser = new GitDiffPatchParser({
141
+ includeGenerated: options.includeGenerated,
142
+ includeBinary: options.includeBinary,
143
+ budget,
144
+ onHunk: (fileId, hunk) => {
145
+ hooks?.emit({
146
+ type: 'hunk',
147
+ fileId,
148
+ hunk,
149
+ });
150
+ const usage = budget.usage();
151
+ hooks?.emit({
152
+ type: 'progress',
153
+ files: usage.files,
154
+ hunks: usage.hunks,
155
+ lines: usage.lines,
156
+ });
157
+ },
158
+ onFile: (file) => {
159
+ hooks?.emit({
160
+ type: 'file',
161
+ file,
162
+ });
163
+ const usage = budget.usage();
164
+ hooks?.emit({
165
+ type: 'progress',
166
+ files: usage.files,
167
+ hunks: usage.hunks,
168
+ lines: usage.lines,
169
+ });
170
+ },
171
+ });
172
+
173
+ const patchResult = await streamGitLines({
174
+ cwd: options.cwd,
175
+ args: buildGitDiffArgs(invocation, 'patch'),
176
+ timeoutMs: options.budget.maxRuntimeMs,
177
+ onBytes: (bytes) => budget.addBytes(bytes).allowed,
178
+ onLine: (line) => parser.pushLine(line),
179
+ });
180
+ if (patchResult.exitCode !== 0 && !patchResult.aborted)
181
+ throw new Error(`git diff --patch failed: ${patchResult.stderr || 'unknown error'}`);
182
+
183
+ const parsed = parser.finish();
184
+ const limitReason: DiffCoverageReason =
185
+ parsed.limitReason === 'none' && patchResult.timedOut ? 'max-runtime-ms' : parsed.limitReason;
186
+ const coverage = resolveCoverage({
187
+ filesObservedInPreflight: preflight.filesChanged,
188
+ filesIncluded: parsed.files.length,
189
+ filesSkippedByFilter: parsed.skippedFiles,
190
+ filesTruncatedByBudget: parsed.truncatedFiles,
191
+ limitReason,
192
+ });
193
+ const generatedAt = new Date().toISOString();
194
+ const diff: NormalizedDiff = {
195
+ spec: {
196
+ diffId: computeDiffId(options.mode, invocation.baseRef, invocation.headRef, parsed.files),
197
+ mode: options.mode,
198
+ baseRef: invocation.baseRef,
199
+ headRef: invocation.headRef,
200
+ generatedAt,
201
+ },
202
+ files: parsed.files,
203
+ totals: parsed.totals,
204
+ coverage,
205
+ };
206
+
207
+ const parseWarnings = [...parsed.warnings];
208
+ const expectedTotal = parsed.files.length + coverage.skippedFiles + coverage.truncatedFiles;
209
+ if (expectedTotal !== preflight.filesChanged) {
210
+ parseWarnings.push(
211
+ `preflight mismatch: filesChanged=${preflight.filesChanged} accounted=${expectedTotal}`,
212
+ );
213
+ }
214
+
215
+ hooks?.emit({
216
+ type: 'coverage',
217
+ coverage,
218
+ });
219
+ hooks?.emit({
220
+ type: 'complete',
221
+ diff,
222
+ });
223
+
224
+ return {
225
+ diff,
226
+ diagnostics: {
227
+ elapsedMs: Math.max(0, Date.now() - startedAtMs),
228
+ peakBufferBytes: patchResult.peakLineBufferBytes,
229
+ parseWarnings,
230
+ },
231
+ };
232
+ }
233
+
234
+ async function* streamDiff(options: DiffBuildOptions): AsyncIterable<DiffStreamEvent> {
235
+ const events: DiffStreamEvent[] = [];
236
+ let notifyNext: (() => void) | null = null;
237
+ let done = false;
238
+ let failure: unknown = null;
239
+
240
+ const notify = (): void => {
241
+ const callback = notifyNext;
242
+ notifyNext = null;
243
+ callback?.();
244
+ };
245
+
246
+ void runBuild(options, {
247
+ emit(event) {
248
+ events.push(event);
249
+ notify();
250
+ },
251
+ })
252
+ .then(() => {
253
+ done = true;
254
+ notify();
255
+ })
256
+ .catch((error: unknown) => {
257
+ failure = error;
258
+ done = true;
259
+ notify();
260
+ });
261
+
262
+ while (true) {
263
+ while (events.length > 0) {
264
+ const event = events.shift();
265
+ if (event !== undefined) {
266
+ yield event;
267
+ }
268
+ }
269
+
270
+ if (failure !== null) {
271
+ throw failure;
272
+ }
273
+
274
+ if (done) {
275
+ return;
276
+ }
277
+
278
+ await new Promise<void>((resolve) => {
279
+ notifyNext = resolve;
280
+ });
281
+ }
282
+ }
283
+
284
+ export function createDiffBuilder(): DiffBuilder {
285
+ return {
286
+ build: async (options: DiffBuildOptions): Promise<DiffBuildResult> => await runBuild(options),
287
+ stream: streamDiff,
288
+ };
289
+ }
@@ -0,0 +1,146 @@
1
+ import { computeDiffChunkId } from './hash.ts';
2
+ import type { ChunkPolicy, DiffChunk, DiffChunker, DiffHunk, NormalizedDiff } from './types.ts';
3
+
4
+ function normalizeChunkPolicy(policy: ChunkPolicy): ChunkPolicy {
5
+ return {
6
+ maxHunksPerChunk: Math.max(1, Math.floor(policy.maxHunksPerChunk)),
7
+ maxLinesPerChunk: Math.max(1, Math.floor(policy.maxLinesPerChunk)),
8
+ maxApproxTokensPerChunk: Math.max(1, Math.floor(policy.maxApproxTokensPerChunk)),
9
+ };
10
+ }
11
+
12
+ function hunkApproxBytes(hunk: DiffHunk): number {
13
+ let bytes = Buffer.byteLength(hunk.header);
14
+ for (const line of hunk.lines) {
15
+ bytes += Buffer.byteLength(line.text) + 1;
16
+ }
17
+ return bytes;
18
+ }
19
+
20
+ function approxTokensFromBytes(bytes: number): number {
21
+ return Math.max(1, Math.ceil(bytes / 4));
22
+ }
23
+
24
+ function policySignature(policy: ChunkPolicy): string {
25
+ return `${policy.maxHunksPerChunk}:${policy.maxLinesPerChunk}:${policy.maxApproxTokensPerChunk}`;
26
+ }
27
+
28
+ function finalizeChunk(
29
+ fileId: string,
30
+ path: string,
31
+ sequence: number,
32
+ totalForFile: number,
33
+ hunks: readonly DiffHunk[],
34
+ policySig: string,
35
+ ): DiffChunk {
36
+ const hunkIds: string[] = [];
37
+ for (const hunk of hunks) {
38
+ hunkIds.push(hunk.hunkId);
39
+ }
40
+ let approxBytes = Buffer.byteLength(path);
41
+ for (const hunk of hunks) {
42
+ approxBytes += hunkApproxBytes(hunk);
43
+ }
44
+ return {
45
+ chunkId: computeDiffChunkId(fileId, sequence, hunkIds, policySig),
46
+ fileId,
47
+ path,
48
+ sequence,
49
+ totalForFile,
50
+ hunkIds,
51
+ approxTokens: approxTokensFromBytes(approxBytes),
52
+ approxBytes,
53
+ payload: {
54
+ fileHeader: path,
55
+ hunks,
56
+ },
57
+ };
58
+ }
59
+
60
+ function chunkSingleFile(
61
+ fileId: string,
62
+ path: string,
63
+ hunks: readonly DiffHunk[],
64
+ policy: ChunkPolicy,
65
+ ): readonly DiffChunk[] {
66
+ const policySig = policySignature(policy);
67
+ const chunksHunks: DiffHunk[][] = [];
68
+ let current: DiffHunk[] = [];
69
+ let currentLines = 0;
70
+ let currentApproxTokens = 0;
71
+
72
+ for (const hunk of hunks) {
73
+ const hunkLines = hunk.lineCount;
74
+ const hunkTokens = approxTokensFromBytes(hunkApproxBytes(hunk));
75
+ const wouldExceed =
76
+ current.length >= policy.maxHunksPerChunk ||
77
+ currentLines + hunkLines > policy.maxLinesPerChunk ||
78
+ currentApproxTokens + hunkTokens > policy.maxApproxTokensPerChunk;
79
+ if (wouldExceed && current.length > 0) {
80
+ chunksHunks.push(current);
81
+ current = [];
82
+ currentLines = 0;
83
+ currentApproxTokens = 0;
84
+ }
85
+ current.push(hunk);
86
+ currentLines += hunkLines;
87
+ currentApproxTokens += hunkTokens;
88
+ const mustFlush =
89
+ current.length >= policy.maxHunksPerChunk ||
90
+ currentLines >= policy.maxLinesPerChunk ||
91
+ currentApproxTokens >= policy.maxApproxTokensPerChunk;
92
+ if (mustFlush) {
93
+ chunksHunks.push(current);
94
+ current = [];
95
+ currentLines = 0;
96
+ currentApproxTokens = 0;
97
+ }
98
+ }
99
+ if (current.length > 0) {
100
+ chunksHunks.push(current);
101
+ }
102
+
103
+ const totalForFile = chunksHunks.length;
104
+ const chunks: DiffChunk[] = [];
105
+ for (let index = 0; index < chunksHunks.length; index += 1) {
106
+ const hunkChunk = chunksHunks[index]!;
107
+ chunks.push(finalizeChunk(fileId, path, index + 1, totalForFile, hunkChunk, policySig));
108
+ }
109
+ return chunks;
110
+ }
111
+
112
+ function chunkDiff(diff: NormalizedDiff, policy: ChunkPolicy): readonly DiffChunk[] {
113
+ const normalizedPolicy = normalizeChunkPolicy(policy);
114
+ const chunks: DiffChunk[] = [];
115
+ for (const file of diff.files) {
116
+ if (file.isBinary || file.hunks.length === 0) {
117
+ continue;
118
+ }
119
+ const path = file.newPath ?? file.oldPath ?? file.fileId;
120
+ chunks.push(...chunkSingleFile(file.fileId, path, file.hunks, normalizedPolicy));
121
+ }
122
+ return chunks;
123
+ }
124
+
125
+ async function* streamDiffChunks(
126
+ diff: NormalizedDiff,
127
+ policy: ChunkPolicy,
128
+ ): AsyncIterable<DiffChunk> {
129
+ const normalizedPolicy = normalizeChunkPolicy(policy);
130
+ for (const file of diff.files) {
131
+ if (file.isBinary || file.hunks.length === 0) {
132
+ continue;
133
+ }
134
+ const path = file.newPath ?? file.oldPath ?? file.fileId;
135
+ for (const chunk of chunkSingleFile(file.fileId, path, file.hunks, normalizedPolicy)) {
136
+ yield chunk;
137
+ }
138
+ }
139
+ }
140
+
141
+ export function createDiffChunker(): DiffChunker {
142
+ return {
143
+ chunk: chunkDiff,
144
+ streamChunks: streamDiffChunks,
145
+ };
146
+ }