@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,178 @@
1
+ export type DiffMode = 'unstaged' | 'staged' | 'range';
2
+
3
+ export type FileChangeType =
4
+ | 'added'
5
+ | 'modified'
6
+ | 'deleted'
7
+ | 'renamed'
8
+ | 'copied'
9
+ | 'binary'
10
+ | 'submodule'
11
+ | 'type-change'
12
+ | 'unknown';
13
+
14
+ export type DiffCoverageReason =
15
+ | 'none'
16
+ | 'max-files'
17
+ | 'max-lines'
18
+ | 'max-hunks'
19
+ | 'max-bytes'
20
+ | 'max-runtime-ms';
21
+
22
+ export interface DiffSpec {
23
+ readonly diffId: string;
24
+ readonly mode: DiffMode;
25
+ readonly baseRef: string | null;
26
+ readonly headRef: string | null;
27
+ readonly generatedAt: string;
28
+ }
29
+
30
+ export interface DiffLine {
31
+ readonly kind: 'context' | 'add' | 'del';
32
+ readonly oldLine: number | null;
33
+ readonly newLine: number | null;
34
+ readonly text: string;
35
+ }
36
+
37
+ export interface DiffHunk {
38
+ readonly hunkId: string;
39
+ readonly oldStart: number;
40
+ readonly oldCount: number;
41
+ readonly newStart: number;
42
+ readonly newCount: number;
43
+ readonly header: string;
44
+ readonly lines: readonly DiffLine[];
45
+ readonly lineCount: number;
46
+ readonly addCount: number;
47
+ readonly delCount: number;
48
+ }
49
+
50
+ export interface DiffFile {
51
+ readonly fileId: string;
52
+ readonly changeType: FileChangeType;
53
+ readonly oldPath: string | null;
54
+ readonly newPath: string | null;
55
+ readonly language: string | null;
56
+ readonly isBinary: boolean;
57
+ readonly isGenerated: boolean;
58
+ readonly isTooLarge: boolean;
59
+ readonly additions: number;
60
+ readonly deletions: number;
61
+ readonly hunks: readonly DiffHunk[];
62
+ }
63
+
64
+ export interface DiffTotals {
65
+ readonly filesChanged: number;
66
+ readonly additions: number;
67
+ readonly deletions: number;
68
+ readonly binaryFiles: number;
69
+ readonly generatedFiles: number;
70
+ readonly hunks: number;
71
+ readonly lines: number;
72
+ }
73
+
74
+ export interface DiffCoverage {
75
+ readonly complete: boolean;
76
+ readonly truncated: boolean;
77
+ readonly skippedFiles: number;
78
+ readonly truncatedFiles: number;
79
+ readonly reason: DiffCoverageReason;
80
+ }
81
+
82
+ export interface NormalizedDiff {
83
+ readonly spec: DiffSpec;
84
+ readonly files: readonly DiffFile[];
85
+ readonly totals: DiffTotals;
86
+ readonly coverage: DiffCoverage;
87
+ }
88
+
89
+ export interface DiffChunk {
90
+ readonly chunkId: string;
91
+ readonly fileId: string;
92
+ readonly path: string;
93
+ readonly sequence: number;
94
+ readonly totalForFile: number;
95
+ readonly hunkIds: readonly string[];
96
+ readonly approxTokens: number;
97
+ readonly approxBytes: number;
98
+ readonly payload: {
99
+ readonly fileHeader: string;
100
+ readonly hunks: readonly DiffHunk[];
101
+ };
102
+ }
103
+
104
+ export interface DiffBudget {
105
+ readonly maxFiles: number;
106
+ readonly maxHunks: number;
107
+ readonly maxLines: number;
108
+ readonly maxBytes: number;
109
+ readonly maxRuntimeMs: number;
110
+ }
111
+
112
+ export interface DiffBuildOptions {
113
+ readonly cwd: string;
114
+ readonly mode: DiffMode;
115
+ readonly baseRef?: string;
116
+ readonly headRef?: string;
117
+ readonly includeGenerated?: boolean;
118
+ readonly includeBinary?: boolean;
119
+ readonly budget: DiffBudget;
120
+ readonly git?: {
121
+ readonly noRenames?: boolean;
122
+ readonly renameLimit?: number;
123
+ };
124
+ }
125
+
126
+ export interface DiffBuildDiagnostics {
127
+ readonly elapsedMs: number;
128
+ readonly peakBufferBytes: number;
129
+ readonly parseWarnings: readonly string[];
130
+ }
131
+
132
+ export interface DiffBuildResult {
133
+ readonly diff: NormalizedDiff;
134
+ readonly diagnostics: DiffBuildDiagnostics;
135
+ }
136
+
137
+ export type DiffStreamEvent =
138
+ | { readonly type: 'start'; readonly mode: DiffMode }
139
+ | { readonly type: 'file'; readonly file: DiffFile }
140
+ | { readonly type: 'hunk'; readonly fileId: string; readonly hunk: DiffHunk }
141
+ | {
142
+ readonly type: 'progress';
143
+ readonly files: number;
144
+ readonly hunks: number;
145
+ readonly lines: number;
146
+ }
147
+ | { readonly type: 'coverage'; readonly coverage: DiffCoverage }
148
+ | { readonly type: 'complete'; readonly diff: NormalizedDiff };
149
+
150
+ export interface ChunkPolicy {
151
+ readonly maxHunksPerChunk: number;
152
+ readonly maxLinesPerChunk: number;
153
+ readonly maxApproxTokensPerChunk: number;
154
+ }
155
+
156
+ export interface DiffBuilder {
157
+ build(options: DiffBuildOptions): Promise<DiffBuildResult>;
158
+ stream(options: DiffBuildOptions): AsyncIterable<DiffStreamEvent>;
159
+ }
160
+
161
+ export interface DiffChunker {
162
+ chunk(diff: NormalizedDiff, policy: ChunkPolicy): readonly DiffChunk[];
163
+ streamChunks(diff: NormalizedDiff, policy: ChunkPolicy): AsyncIterable<DiffChunk>;
164
+ }
165
+
166
+ export const DEFAULT_DIFF_BUDGET: DiffBudget = {
167
+ maxFiles: 3000,
168
+ maxHunks: 20000,
169
+ maxLines: 300000,
170
+ maxBytes: 64 * 1024 * 1024,
171
+ maxRuntimeMs: 60_000,
172
+ };
173
+
174
+ export const DEFAULT_CHUNK_POLICY: ChunkPolicy = {
175
+ maxHunksPerChunk: 8,
176
+ maxLinesPerChunk: 800,
177
+ maxApproxTokensPerChunk: 4000,
178
+ };
@@ -0,0 +1,346 @@
1
+ import { resolve } from 'node:path';
2
+ import { DEFAULT_DIFF_BUDGET, type DiffMode } from '../diff/types.ts';
3
+ import type {
4
+ DiffUiCliOptions,
5
+ DiffUiSyntaxMode,
6
+ DiffUiViewMode,
7
+ DiffUiWordDiffMode,
8
+ } from './types.ts';
9
+
10
+ interface ParseDiffUiArgsOptions {
11
+ readonly cwd?: string;
12
+ readonly env?: NodeJS.ProcessEnv;
13
+ readonly isStdoutTty?: boolean;
14
+ }
15
+
16
+ interface MutableDiffUiBudget {
17
+ maxFiles: number;
18
+ maxHunks: number;
19
+ maxLines: number;
20
+ maxBytes: number;
21
+ maxRuntimeMs: number;
22
+ }
23
+
24
+ function parseFiniteInt(value: string, flag: string): number {
25
+ const trimmed = value.trim();
26
+ if (!/^\d+$/u.test(trimmed)) {
27
+ throw new Error(`invalid ${flag} value: ${value}`);
28
+ }
29
+ return Number.parseInt(trimmed, 10);
30
+ }
31
+
32
+ function parsePositiveInt(value: string, flag: string): number {
33
+ const parsed = parseFiniteInt(value, flag);
34
+ if (parsed <= 0) {
35
+ throw new Error(`${flag} must be greater than zero`);
36
+ }
37
+ return parsed;
38
+ }
39
+
40
+ function readNextValue(argv: readonly string[], index: number, flag: string): string {
41
+ const next = argv[index + 1];
42
+ if (next === undefined) {
43
+ throw new Error(`missing value for ${flag}`);
44
+ }
45
+ return next;
46
+ }
47
+
48
+ function hasFlagPrefix(value: string): boolean {
49
+ return value.startsWith('--');
50
+ }
51
+
52
+ function parseViewMode(raw: string): DiffUiViewMode {
53
+ if (raw === 'auto' || raw === 'split' || raw === 'unified') {
54
+ return raw;
55
+ }
56
+ throw new Error(`invalid --view value: ${raw}`);
57
+ }
58
+
59
+ function parseSyntaxMode(raw: string): DiffUiSyntaxMode {
60
+ if (raw === 'auto' || raw === 'on' || raw === 'off') {
61
+ return raw;
62
+ }
63
+ throw new Error(`invalid --syntax value: ${raw}`);
64
+ }
65
+
66
+ function parseWordDiffMode(raw: string): DiffUiWordDiffMode {
67
+ if (raw === 'auto' || raw === 'on' || raw === 'off') {
68
+ return raw;
69
+ }
70
+ throw new Error(`invalid --word-diff value: ${raw}`);
71
+ }
72
+
73
+ function cloneBudget(): MutableDiffUiBudget {
74
+ return {
75
+ maxFiles: DEFAULT_DIFF_BUDGET.maxFiles,
76
+ maxHunks: DEFAULT_DIFF_BUDGET.maxHunks,
77
+ maxLines: DEFAULT_DIFF_BUDGET.maxLines,
78
+ maxBytes: DEFAULT_DIFF_BUDGET.maxBytes,
79
+ maxRuntimeMs: DEFAULT_DIFF_BUDGET.maxRuntimeMs,
80
+ };
81
+ }
82
+
83
+ export function parseDiffUiArgs(
84
+ argv: readonly string[],
85
+ options: ParseDiffUiArgsOptions = {},
86
+ ): DiffUiCliOptions {
87
+ const env = options.env ?? process.env;
88
+ const cwd = options.cwd ?? process.cwd();
89
+ const stdoutIsTty = options.isStdoutTty ?? process.stdout.isTTY === true;
90
+
91
+ let mode: DiffMode = 'unstaged';
92
+ let baseRef: string | null = null;
93
+ let headRef: string | null = null;
94
+
95
+ let includeGenerated = false;
96
+ let includeBinary = false;
97
+ let noRenames = true;
98
+ let renameLimit: number | null = null;
99
+
100
+ let viewMode: DiffUiViewMode = 'auto';
101
+ let syntaxMode: DiffUiSyntaxMode = 'auto';
102
+ let wordDiffMode: DiffUiWordDiffMode = 'auto';
103
+ let color = stdoutIsTty && env.NO_COLOR === undefined;
104
+ let pagerExplicit: boolean | null = null;
105
+ let watch = false;
106
+ let jsonEvents = false;
107
+ let rpcStdio = false;
108
+ let snapshot = false;
109
+ let width: number | null = null;
110
+ let height: number | null = null;
111
+ let theme: string | null = null;
112
+ let resolvedCwd = cwd;
113
+
114
+ const budget = cloneBudget();
115
+
116
+ for (let index = 0; index < argv.length; index += 1) {
117
+ const arg = argv[index]!;
118
+
119
+ if (arg === '--help' || arg === '-h') {
120
+ throw new Error('help requested');
121
+ }
122
+ if (arg === '--staged') {
123
+ mode = 'staged';
124
+ continue;
125
+ }
126
+ if (arg === '--base') {
127
+ const next = argv[index + 1];
128
+ if (next !== undefined && !hasFlagPrefix(next)) {
129
+ baseRef = next;
130
+ index += 1;
131
+ } else {
132
+ baseRef = null;
133
+ }
134
+ mode = 'range';
135
+ continue;
136
+ }
137
+ if (arg === '--head') {
138
+ const value = readNextValue(argv, index, '--head');
139
+ headRef = value;
140
+ index += 1;
141
+ continue;
142
+ }
143
+ if (arg === '--view') {
144
+ const value = readNextValue(argv, index, '--view');
145
+ viewMode = parseViewMode(value);
146
+ index += 1;
147
+ continue;
148
+ }
149
+ if (arg === '--syntax') {
150
+ const value = readNextValue(argv, index, '--syntax');
151
+ syntaxMode = parseSyntaxMode(value);
152
+ index += 1;
153
+ continue;
154
+ }
155
+ if (arg === '--word-diff') {
156
+ const value = readNextValue(argv, index, '--word-diff');
157
+ wordDiffMode = parseWordDiffMode(value);
158
+ index += 1;
159
+ continue;
160
+ }
161
+ if (arg === '--no-color') {
162
+ color = false;
163
+ continue;
164
+ }
165
+ if (arg === '--pager') {
166
+ pagerExplicit = true;
167
+ continue;
168
+ }
169
+ if (arg === '--no-pager') {
170
+ pagerExplicit = false;
171
+ continue;
172
+ }
173
+ if (arg === '--json-events') {
174
+ jsonEvents = true;
175
+ continue;
176
+ }
177
+ if (arg === '--rpc-stdio') {
178
+ rpcStdio = true;
179
+ continue;
180
+ }
181
+ if (arg === '--snapshot') {
182
+ snapshot = true;
183
+ continue;
184
+ }
185
+ if (arg === '--watch') {
186
+ watch = true;
187
+ continue;
188
+ }
189
+ if (arg === '--theme') {
190
+ const value = readNextValue(argv, index, '--theme');
191
+ theme = value;
192
+ index += 1;
193
+ continue;
194
+ }
195
+ if (arg === '--width') {
196
+ const value = readNextValue(argv, index, '--width');
197
+ width = parsePositiveInt(value, '--width');
198
+ index += 1;
199
+ continue;
200
+ }
201
+ if (arg === '--height') {
202
+ const value = readNextValue(argv, index, '--height');
203
+ height = parsePositiveInt(value, '--height');
204
+ index += 1;
205
+ continue;
206
+ }
207
+ if (arg === '--max-files') {
208
+ const value = readNextValue(argv, index, '--max-files');
209
+ budget.maxFiles = parsePositiveInt(value, '--max-files');
210
+ index += 1;
211
+ continue;
212
+ }
213
+ if (arg === '--max-hunks') {
214
+ const value = readNextValue(argv, index, '--max-hunks');
215
+ budget.maxHunks = parsePositiveInt(value, '--max-hunks');
216
+ index += 1;
217
+ continue;
218
+ }
219
+ if (arg === '--max-lines') {
220
+ const value = readNextValue(argv, index, '--max-lines');
221
+ budget.maxLines = parsePositiveInt(value, '--max-lines');
222
+ index += 1;
223
+ continue;
224
+ }
225
+ if (arg === '--max-bytes') {
226
+ const value = readNextValue(argv, index, '--max-bytes');
227
+ budget.maxBytes = parsePositiveInt(value, '--max-bytes');
228
+ index += 1;
229
+ continue;
230
+ }
231
+ if (arg === '--max-runtime-ms') {
232
+ const value = readNextValue(argv, index, '--max-runtime-ms');
233
+ budget.maxRuntimeMs = parsePositiveInt(value, '--max-runtime-ms');
234
+ index += 1;
235
+ continue;
236
+ }
237
+ if (arg === '--include-generated') {
238
+ includeGenerated = true;
239
+ continue;
240
+ }
241
+ if (arg === '--include-binary') {
242
+ includeBinary = true;
243
+ continue;
244
+ }
245
+ if (arg === '--renames') {
246
+ noRenames = false;
247
+ continue;
248
+ }
249
+ if (arg === '--no-renames') {
250
+ noRenames = true;
251
+ continue;
252
+ }
253
+ if (arg === '--rename-limit') {
254
+ const value = readNextValue(argv, index, '--rename-limit');
255
+ renameLimit = parseFiniteInt(value, '--rename-limit');
256
+ noRenames = false;
257
+ index += 1;
258
+ continue;
259
+ }
260
+ if (arg === '--cwd') {
261
+ const value = readNextValue(argv, index, '--cwd');
262
+ resolvedCwd = resolve(cwd, value);
263
+ index += 1;
264
+ continue;
265
+ }
266
+
267
+ throw new Error(`unknown option: ${arg}`);
268
+ }
269
+
270
+ if (mode === 'range' && headRef === null) {
271
+ headRef = 'HEAD';
272
+ }
273
+ if (mode !== 'range' && (baseRef !== null || headRef !== null)) {
274
+ throw new Error('--base/--head are only valid for range mode');
275
+ }
276
+ const pagerIncompatibleMode = jsonEvents || rpcStdio || snapshot;
277
+ const pager = pagerExplicit === null ? stdoutIsTty && !pagerIncompatibleMode : pagerExplicit;
278
+ if (pager && pagerIncompatibleMode) {
279
+ throw new Error('--pager cannot be combined with --json-events, --rpc-stdio, or --snapshot');
280
+ }
281
+
282
+ return {
283
+ cwd: resolvedCwd,
284
+ mode,
285
+ baseRef,
286
+ headRef,
287
+ includeGenerated,
288
+ includeBinary,
289
+ noRenames,
290
+ renameLimit,
291
+ viewMode,
292
+ syntaxMode,
293
+ wordDiffMode,
294
+ color,
295
+ pager,
296
+ watch,
297
+ jsonEvents,
298
+ rpcStdio,
299
+ snapshot,
300
+ width,
301
+ height,
302
+ theme,
303
+ budget,
304
+ };
305
+ }
306
+
307
+ export function diffUiUsage(): string {
308
+ return [
309
+ 'usage: harness diff [options]',
310
+ '',
311
+ 'diff source:',
312
+ ' --staged',
313
+ ' --base [<ref>] [--head <ref>]',
314
+ '',
315
+ 'display:',
316
+ ' --view <auto|split|unified>',
317
+ ' --syntax <auto|on|off>',
318
+ ' --word-diff <auto|on|off>',
319
+ ' --theme <name>',
320
+ ' --no-color',
321
+ ' --pager',
322
+ ' --no-pager',
323
+ ' --width <n>',
324
+ ' --height <n>',
325
+ '',
326
+ 'runtime:',
327
+ ' --json-events',
328
+ ' --rpc-stdio',
329
+ ' --snapshot',
330
+ ' --watch',
331
+ '',
332
+ 'budgets:',
333
+ ' --max-files <n>',
334
+ ' --max-hunks <n>',
335
+ ' --max-lines <n>',
336
+ ' --max-bytes <n>',
337
+ ' --max-runtime-ms <n>',
338
+ '',
339
+ 'git:',
340
+ ' --include-generated',
341
+ ' --include-binary',
342
+ ' --renames | --no-renames',
343
+ ' --rename-limit <n>',
344
+ ' --cwd <path>',
345
+ ].join('\n');
346
+ }
@@ -0,0 +1,123 @@
1
+ import type { DiffUiCommand, DiffUiStateAction } from './types.ts';
2
+
3
+ export function parseDiffUiCommand(value: unknown): DiffUiCommand | null {
4
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
5
+ return null;
6
+ }
7
+ const record = value as Record<string, unknown>;
8
+ const type = record['type'];
9
+ if (typeof type !== 'string') {
10
+ return null;
11
+ }
12
+
13
+ if (type === 'view.setMode') {
14
+ const mode = record['mode'];
15
+ if (mode === 'auto' || mode === 'split' || mode === 'unified') {
16
+ return { type, mode };
17
+ }
18
+ return null;
19
+ }
20
+ if (type === 'nav.scroll') {
21
+ const delta = record['delta'];
22
+ return typeof delta === 'number' && Number.isFinite(delta) ? { type, delta } : null;
23
+ }
24
+ if (type === 'nav.page') {
25
+ const delta = record['delta'];
26
+ return typeof delta === 'number' && Number.isFinite(delta) ? { type, delta } : null;
27
+ }
28
+ if (type === 'nav.gotoFile' || type === 'nav.gotoHunk') {
29
+ const index = record['index'];
30
+ if (typeof index !== 'number' || !Number.isFinite(index)) {
31
+ return null;
32
+ }
33
+ if (type === 'nav.gotoFile') {
34
+ return { type, index };
35
+ }
36
+ return { type, index };
37
+ }
38
+ if (type === 'finder.open' || type === 'finder.close' || type === 'finder.accept') {
39
+ return { type };
40
+ }
41
+ if (type === 'finder.query' || type === 'search.set') {
42
+ const query = record['query'];
43
+ if (typeof query !== 'string') {
44
+ return null;
45
+ }
46
+ if (type === 'finder.query') {
47
+ return { type, query };
48
+ }
49
+ return { type, query };
50
+ }
51
+ if (type === 'finder.move') {
52
+ const delta = record['delta'];
53
+ return typeof delta === 'number' && Number.isFinite(delta) ? { type, delta } : null;
54
+ }
55
+ if (type === 'session.quit') {
56
+ return { type };
57
+ }
58
+
59
+ return null;
60
+ }
61
+
62
+ type DiffUiActionCommand = Exclude<DiffUiCommand, { readonly type: 'session.quit' }>;
63
+
64
+ export function diffUiCommandToStateAction(
65
+ command: DiffUiActionCommand,
66
+ pageSize: number,
67
+ ): DiffUiStateAction {
68
+ switch (command.type) {
69
+ case 'view.setMode':
70
+ return {
71
+ type: 'view.setMode',
72
+ mode: command.mode,
73
+ };
74
+ case 'nav.scroll':
75
+ return {
76
+ type: 'nav.scroll',
77
+ delta: Math.trunc(command.delta),
78
+ };
79
+ case 'nav.page':
80
+ return {
81
+ type: 'nav.page',
82
+ delta: Math.trunc(command.delta),
83
+ pageSize,
84
+ };
85
+ case 'nav.gotoFile':
86
+ return {
87
+ type: 'nav.gotoFile',
88
+ fileIndex: Math.trunc(command.index),
89
+ };
90
+ case 'nav.gotoHunk':
91
+ return {
92
+ type: 'nav.gotoHunk',
93
+ hunkIndex: Math.trunc(command.index),
94
+ };
95
+ case 'finder.open':
96
+ return {
97
+ type: 'finder.open',
98
+ };
99
+ case 'finder.close':
100
+ return {
101
+ type: 'finder.close',
102
+ };
103
+ case 'finder.query':
104
+ return {
105
+ type: 'finder.query',
106
+ query: command.query,
107
+ };
108
+ case 'finder.move':
109
+ return {
110
+ type: 'finder.move',
111
+ delta: Math.trunc(command.delta),
112
+ };
113
+ case 'finder.accept':
114
+ return {
115
+ type: 'finder.accept',
116
+ };
117
+ case 'search.set':
118
+ return {
119
+ type: 'search.set',
120
+ query: command.query,
121
+ };
122
+ }
123
+ }