ubume 0.1.0

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 (182) hide show
  1. package/README.md +88 -0
  2. package/bin/codexa-local-harness-bridge.js +3 -0
  3. package/bin/codexa.js +28 -0
  4. package/bin/ubume-local-harness-bridge.js +325 -0
  5. package/bin/ubume.js +398 -0
  6. package/package.json +66 -0
  7. package/src/app.tsx +5759 -0
  8. package/src/commands/handler.ts +889 -0
  9. package/src/config/appVersion.ts +69 -0
  10. package/src/config/buildInfo.ts +3 -0
  11. package/src/config/launchArgs.ts +196 -0
  12. package/src/config/layeredConfig.ts +853 -0
  13. package/src/config/legacyEnv.ts +16 -0
  14. package/src/config/persistence.ts +377 -0
  15. package/src/config/runtimeConfig.ts +558 -0
  16. package/src/config/settings.ts +405 -0
  17. package/src/config/toml-serialize.ts +109 -0
  18. package/src/config/trustStore.ts +84 -0
  19. package/src/config/updateCheckCache.ts +85 -0
  20. package/src/core/README.md +55 -0
  21. package/src/core/agent/loop.ts +464 -0
  22. package/src/core/agent/protocol.ts +345 -0
  23. package/src/core/agent/tools.ts +423 -0
  24. package/src/core/auth/codexAuth.ts +359 -0
  25. package/src/core/codex/codexExecArgs.ts +166 -0
  26. package/src/core/codex/codexLaunch.ts +163 -0
  27. package/src/core/codex/codexPrompt.ts +429 -0
  28. package/src/core/debug/inputDebug.ts +51 -0
  29. package/src/core/debug/localStreamDebug.ts +50 -0
  30. package/src/core/debug/modelStateDebug.ts +35 -0
  31. package/src/core/executables/antigravityExecutable.ts +48 -0
  32. package/src/core/executables/claudeExecutable.ts +63 -0
  33. package/src/core/executables/codexExecutable.ts +160 -0
  34. package/src/core/executables/executableResolver.ts +164 -0
  35. package/src/core/executables/geminiExecutable.ts +78 -0
  36. package/src/core/models/codexCapabilities.ts +97 -0
  37. package/src/core/models/codexModelCapabilities.ts +624 -0
  38. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  39. package/src/core/models/modelSpecs.ts +303 -0
  40. package/src/core/models/providerModelCache.ts +94 -0
  41. package/src/core/perf/profiler.ts +125 -0
  42. package/src/core/perf/renderDebug.ts +398 -0
  43. package/src/core/process/CommandRunner.ts +280 -0
  44. package/src/core/process/processValidation.ts +111 -0
  45. package/src/core/providerLauncher/launcher.ts +220 -0
  46. package/src/core/providerLauncher/registry.ts +354 -0
  47. package/src/core/providerLauncher/types.ts +95 -0
  48. package/src/core/providerLauncher/workspaceConfig.ts +487 -0
  49. package/src/core/providerRuntime/anthropic.ts +580 -0
  50. package/src/core/providerRuntime/antigravity.ts +500 -0
  51. package/src/core/providerRuntime/capabilityProfile.ts +383 -0
  52. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
  53. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  54. package/src/core/providerRuntime/codexaCupy.ts +97 -0
  55. package/src/core/providerRuntime/codexaNative.ts +425 -0
  56. package/src/core/providerRuntime/contextMetadata.ts +397 -0
  57. package/src/core/providerRuntime/gemini.ts +789 -0
  58. package/src/core/providerRuntime/lmstudio.ts +118 -0
  59. package/src/core/providerRuntime/local.ts +770 -0
  60. package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
  61. package/src/core/providerRuntime/localOutputBudget.ts +17 -0
  62. package/src/core/providerRuntime/mistralVibe.ts +667 -0
  63. package/src/core/providerRuntime/models.ts +175 -0
  64. package/src/core/providerRuntime/reasoning.ts +20 -0
  65. package/src/core/providerRuntime/registry.ts +286 -0
  66. package/src/core/providerRuntime/types.ts +142 -0
  67. package/src/core/providerRuntime/unsloth.ts +216 -0
  68. package/src/core/providers/codexJsonStream.ts +305 -0
  69. package/src/core/providers/codexSubprocess.ts +378 -0
  70. package/src/core/providers/codexTranscript.ts +695 -0
  71. package/src/core/providers/openaiNative.ts +13 -0
  72. package/src/core/providers/registry.ts +21 -0
  73. package/src/core/providers/types.ts +91 -0
  74. package/src/core/shared/attachments.ts +101 -0
  75. package/src/core/shared/cleanupFastFail.ts +67 -0
  76. package/src/core/shared/clipboard.ts +24 -0
  77. package/src/core/shared/clipboardImage.ts +111 -0
  78. package/src/core/shared/githubDiagnostics.ts +222 -0
  79. package/src/core/shared/hollowResponseFormat.ts +39 -0
  80. package/src/core/terminal/clearFrameBoundary.ts +852 -0
  81. package/src/core/terminal/frameLock.ts +110 -0
  82. package/src/core/terminal/inkRenderReset.ts +123 -0
  83. package/src/core/terminal/startupClear.ts +20 -0
  84. package/src/core/terminal/terminalCapabilities.ts +100 -0
  85. package/src/core/terminal/terminalControl.ts +169 -0
  86. package/src/core/terminal/terminalSanitize.ts +147 -0
  87. package/src/core/terminal/terminalTitle.ts +400 -0
  88. package/src/core/version/channel.ts +27 -0
  89. package/src/core/version/packageManager.ts +119 -0
  90. package/src/core/version/updateCheck.ts +203 -0
  91. package/src/core/workspace/appData.ts +107 -0
  92. package/src/core/workspace/conversationStore.ts +335 -0
  93. package/src/core/workspace/launchContext.ts +259 -0
  94. package/src/core/workspace/planStorage.ts +135 -0
  95. package/src/core/workspace/projectInstructions.ts +54 -0
  96. package/src/core/workspace/scratchDir.ts +64 -0
  97. package/src/core/workspace/workspaceActivity.ts +384 -0
  98. package/src/core/workspace/workspaceGuard.ts +377 -0
  99. package/src/core/workspace/workspaceRoot.ts +47 -0
  100. package/src/exec.ts +73 -0
  101. package/src/headless/execArgs.ts +296 -0
  102. package/src/headless/execRunner.ts +304 -0
  103. package/src/index.tsx +270 -0
  104. package/src/legacyEnvBootstrap.ts +5 -0
  105. package/src/session/appSession.ts +771 -0
  106. package/src/session/chatLifecycle.ts +994 -0
  107. package/src/session/conversation.ts +107 -0
  108. package/src/session/liveRenderScheduler.ts +214 -0
  109. package/src/session/persistedResponse.ts +93 -0
  110. package/src/session/planFlow.ts +159 -0
  111. package/src/session/planTranscript.ts +19 -0
  112. package/src/session/promptRunSchedule.ts +26 -0
  113. package/src/session/types.ts +234 -0
  114. package/src/test/runtimeTestUtils.ts +14 -0
  115. package/src/types/react-dom.d.ts +3 -0
  116. package/src/ui/chrome/ActivityBars.tsx +68 -0
  117. package/src/ui/chrome/ActivityIndicator.tsx +58 -0
  118. package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
  119. package/src/ui/chrome/AppShell.tsx +474 -0
  120. package/src/ui/chrome/BottomComposer.tsx +1135 -0
  121. package/src/ui/chrome/DashCard.tsx +82 -0
  122. package/src/ui/chrome/RunFooter.tsx +65 -0
  123. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  124. package/src/ui/chrome/Spinner.tsx +25 -0
  125. package/src/ui/chrome/TopHeader.tsx +439 -0
  126. package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
  127. package/src/ui/chrome/busyStatusAnimation.ts +11 -0
  128. package/src/ui/input/commandNormalize.ts +66 -0
  129. package/src/ui/input/focus.ts +73 -0
  130. package/src/ui/input/imageAttachments.ts +18 -0
  131. package/src/ui/input/inputBuffer.ts +203 -0
  132. package/src/ui/input/pastedContent.ts +75 -0
  133. package/src/ui/input/rawArrowKeys.ts +28 -0
  134. package/src/ui/input/slashCommands.ts +43 -0
  135. package/src/ui/input/useStdinRawModeLease.ts +23 -0
  136. package/src/ui/layout.ts +560 -0
  137. package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
  138. package/src/ui/panels/AuthPanel.tsx +149 -0
  139. package/src/ui/panels/BackendPicker.tsx +28 -0
  140. package/src/ui/panels/ModePicker.tsx +31 -0
  141. package/src/ui/panels/ModelPicker.tsx +31 -0
  142. package/src/ui/panels/ModelPickerScreen.tsx +761 -0
  143. package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
  144. package/src/ui/panels/Panel.tsx +51 -0
  145. package/src/ui/panels/PermissionsPanel.tsx +78 -0
  146. package/src/ui/panels/PlanActionPicker.tsx +187 -0
  147. package/src/ui/panels/ProviderPicker.tsx +753 -0
  148. package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
  149. package/src/ui/panels/ReasoningPicker.tsx +46 -0
  150. package/src/ui/panels/ResumePicker.tsx +90 -0
  151. package/src/ui/panels/SelectionPanel.tsx +138 -0
  152. package/src/ui/panels/SettingsPanel.tsx +156 -0
  153. package/src/ui/panels/TextEntryPanel.tsx +139 -0
  154. package/src/ui/panels/ThemePicker.tsx +32 -0
  155. package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
  156. package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
  157. package/src/ui/panels/responsivePickerViewport.ts +64 -0
  158. package/src/ui/render/Markdown.tsx +331 -0
  159. package/src/ui/render/diffRenderer.ts +116 -0
  160. package/src/ui/render/logoVariants.ts +113 -0
  161. package/src/ui/render/modeDisplay.ts +52 -0
  162. package/src/ui/render/outputPipeline.ts +64 -0
  163. package/src/ui/render/runtimeDisplay.ts +128 -0
  164. package/src/ui/render/terminalAnswerFormat.ts +128 -0
  165. package/src/ui/render/textLayout.ts +392 -0
  166. package/src/ui/theme.tsx +274 -0
  167. package/src/ui/themeFlow.ts +41 -0
  168. package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
  169. package/src/ui/timeline/AgentBlock.tsx +130 -0
  170. package/src/ui/timeline/StaticIntroItem.tsx +54 -0
  171. package/src/ui/timeline/ThinkingBlock.tsx +100 -0
  172. package/src/ui/timeline/Timeline.tsx +1410 -0
  173. package/src/ui/timeline/TranscriptShell.tsx +302 -0
  174. package/src/ui/timeline/TurnGroup.tsx +673 -0
  175. package/src/ui/timeline/layoutListWindow.ts +145 -0
  176. package/src/ui/timeline/liveViewportWindow.ts +68 -0
  177. package/src/ui/timeline/progressEntries.ts +156 -0
  178. package/src/ui/timeline/runActivityView.ts +37 -0
  179. package/src/ui/timeline/staticTranscriptCache.ts +174 -0
  180. package/src/ui/timeline/streamCoalesce.ts +53 -0
  181. package/src/ui/timeline/timelineMeasure.ts +3273 -0
  182. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,384 @@
1
+ import { readdirSync, readFileSync, statSync } from "fs";
2
+ import { join, relative, sep } from "path";
3
+ import { SCRATCH_RELATIVE_DIR } from "./scratchDir.js";
4
+
5
+ export type RunFileOperation = "created" | "modified" | "deleted";
6
+ export type RunDiffLineKind = "added" | "removed";
7
+
8
+ export interface RunDiffLine {
9
+ kind: RunDiffLineKind;
10
+ text: string;
11
+ }
12
+
13
+ export interface RunFileActivity {
14
+ path: string;
15
+ operation: RunFileOperation;
16
+ detectedAt: number;
17
+ addedLines?: number;
18
+ removedLines?: number;
19
+ diffLines?: RunDiffLine[];
20
+ }
21
+
22
+ export interface RunActivitySummary {
23
+ created: number;
24
+ modified: number;
25
+ deleted: number;
26
+ totalActivityCount: number;
27
+ recent: RunFileActivity[];
28
+ }
29
+
30
+ export interface WorkspaceActivityTracker {
31
+ stop: () => void;
32
+ }
33
+
34
+ export interface WorkspaceActivityTrackerOptions {
35
+ rootDir: string;
36
+ onActivity: (activity: RunFileActivity[]) => void;
37
+ pollIntervalMs?: number;
38
+ initialSnapshot?: Map<string, WorkspaceFileSnapshot>;
39
+ }
40
+
41
+ export interface WorkspaceFileSnapshot {
42
+ path: string;
43
+ mtimeMs: number;
44
+ size: number;
45
+ content?: string;
46
+ }
47
+
48
+ const DEFAULT_POLL_INTERVAL_MS = 400;
49
+ const MAX_TRACKED_TEXT_BYTES = 128 * 1024;
50
+ const MAX_DIFFABLE_LINE_COUNT = 240;
51
+ const MAX_DIFF_PREVIEW_LINES = 6;
52
+ const MAX_SUMMARY_ACTIVITY_ITEMS = 6;
53
+
54
+ const IGNORED_DIRECTORY_NAMES = new Set([
55
+ ".cache",
56
+ ".git",
57
+ ".next",
58
+ ".parcel-cache",
59
+ ".turbo",
60
+ ".vercel",
61
+ ".yarn",
62
+ "build",
63
+ "coverage",
64
+ "dist",
65
+ "node_modules",
66
+ "out",
67
+ "temp",
68
+ "tmp",
69
+ ]);
70
+
71
+ const BINARY_EXTENSIONS = new Set([
72
+ ".bmp",
73
+ ".class",
74
+ ".dll",
75
+ ".dylib",
76
+ ".exe",
77
+ ".gif",
78
+ ".ico",
79
+ ".jar",
80
+ ".jpeg",
81
+ ".jpg",
82
+ ".lockb",
83
+ ".mp3",
84
+ ".mp4",
85
+ ".o",
86
+ ".otf",
87
+ ".pdf",
88
+ ".png",
89
+ ".so",
90
+ ".ttf",
91
+ ".wasm",
92
+ ".webp",
93
+ ".woff",
94
+ ".woff2",
95
+ ".zip",
96
+ ]);
97
+
98
+ function normalizePath(path: string): string {
99
+ return path.split(sep).join("/");
100
+ }
101
+
102
+ function shouldIgnoreDirectory(name: string): boolean {
103
+ return IGNORED_DIRECTORY_NAMES.has(name);
104
+ }
105
+
106
+ function isBinaryBuffer(buffer: Buffer): boolean {
107
+ return buffer.includes(0);
108
+ }
109
+
110
+ function shouldTrackTextContent(path: string, size: number): boolean {
111
+ const lower = path.toLowerCase();
112
+ const dotIndex = lower.lastIndexOf(".");
113
+ const ext = dotIndex >= 0 ? lower.slice(dotIndex) : "";
114
+ if (size > MAX_TRACKED_TEXT_BYTES) return false;
115
+ if (BINARY_EXTENSIONS.has(ext)) return false;
116
+ return true;
117
+ }
118
+
119
+ function readTrackedFileContent(path: string, size: number): string | undefined {
120
+ if (!shouldTrackTextContent(path, size)) return undefined;
121
+
122
+ const buffer = readFileSync(path);
123
+ if (isBinaryBuffer(buffer)) return undefined;
124
+ return buffer.toString("utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
125
+ }
126
+
127
+ function splitTextLines(text: string): string[] {
128
+ if (!text) return [];
129
+ const lines = text.split("\n");
130
+ if (lines.at(-1) === "") {
131
+ lines.pop();
132
+ }
133
+ return lines;
134
+ }
135
+
136
+ interface DiffOperation {
137
+ kind: "equal" | RunDiffLineKind;
138
+ text: string;
139
+ }
140
+
141
+ // LCS diff has O(n*m) complexity. Skip diffing for large files to avoid
142
+ // blocking the poll loop — callers treat null as "too large to diff".
143
+ function buildLineDiff(before: string[], after: string[]): DiffOperation[] | null {
144
+ if (before.length + after.length > MAX_DIFFABLE_LINE_COUNT) {
145
+ return null;
146
+ }
147
+
148
+ const rows = before.length + 1;
149
+ const cols = after.length + 1;
150
+ const matrix = Array.from({ length: rows }, () => Array<number>(cols).fill(0));
151
+
152
+ for (let row = before.length - 1; row >= 0; row -= 1) {
153
+ for (let col = after.length - 1; col >= 0; col -= 1) {
154
+ if (before[row] === after[col]) {
155
+ matrix[row]![col] = 1 + matrix[row + 1]![col + 1]!;
156
+ } else {
157
+ matrix[row]![col] = Math.max(matrix[row + 1]![col]!, matrix[row]![col + 1]!);
158
+ }
159
+ }
160
+ }
161
+
162
+ const operations: DiffOperation[] = [];
163
+ let row = 0;
164
+ let col = 0;
165
+ while (row < before.length && col < after.length) {
166
+ if (before[row] === after[col]) {
167
+ operations.push({ kind: "equal", text: before[row]! });
168
+ row += 1;
169
+ col += 1;
170
+ continue;
171
+ }
172
+
173
+ if (matrix[row + 1]![col]! >= matrix[row]![col + 1]!) {
174
+ operations.push({ kind: "removed", text: before[row]! });
175
+ row += 1;
176
+ } else {
177
+ operations.push({ kind: "added", text: after[col]! });
178
+ col += 1;
179
+ }
180
+ }
181
+
182
+ while (row < before.length) {
183
+ operations.push({ kind: "removed", text: before[row]! });
184
+ row += 1;
185
+ }
186
+
187
+ while (col < after.length) {
188
+ operations.push({ kind: "added", text: after[col]! });
189
+ col += 1;
190
+ }
191
+
192
+ return operations;
193
+ }
194
+
195
+ export function createTextDiffExcerpt(
196
+ beforeContent: string,
197
+ afterContent: string,
198
+ ): Pick<RunFileActivity, "addedLines" | "removedLines" | "diffLines"> | null {
199
+ const operations = buildLineDiff(splitTextLines(beforeContent), splitTextLines(afterContent));
200
+ if (!operations) {
201
+ return {
202
+ addedLines: afterContent ? splitTextLines(afterContent).length : 0,
203
+ removedLines: beforeContent ? splitTextLines(beforeContent).length : 0,
204
+ };
205
+ }
206
+
207
+ const diffOperations = operations.filter(
208
+ (operation) => operation.kind === "added" || operation.kind === "removed",
209
+ );
210
+
211
+ const diffLines: RunDiffLine[] = diffOperations
212
+ .slice(0, MAX_DIFF_PREVIEW_LINES)
213
+ .map((operation) => ({
214
+ kind: operation.kind as RunDiffLineKind,
215
+ text: operation.text,
216
+ }));
217
+
218
+ const addedLines = operations.filter((operation) => operation.kind === "added").length;
219
+ const removedLines = operations.filter((operation) => operation.kind === "removed").length;
220
+
221
+ if (addedLines === 0 && removedLines === 0) {
222
+ return null;
223
+ }
224
+
225
+ return {
226
+ addedLines,
227
+ removedLines,
228
+ diffLines,
229
+ };
230
+ }
231
+
232
+ export function captureWorkspaceSnapshot(rootDir: string): Map<string, WorkspaceFileSnapshot> {
233
+ const snapshot = new Map<string, WorkspaceFileSnapshot>();
234
+
235
+ function walk(dir: string): void {
236
+ let entries;
237
+ try {
238
+ entries = readdirSync(dir, { withFileTypes: true });
239
+ } catch {
240
+ // Skip directories we can't read (EPERM, locked temp dirs, etc.)
241
+ return;
242
+ }
243
+
244
+ for (const entry of entries) {
245
+ if (entry.isDirectory()) {
246
+ if (shouldIgnoreDirectory(entry.name)) continue;
247
+ const childDir = join(dir, entry.name);
248
+ const normalizedRel = normalizePath(relative(rootDir, childDir));
249
+ if (normalizedRel === SCRATCH_RELATIVE_DIR || normalizedRel === ".codexa/scratch") continue;
250
+ walk(childDir);
251
+ continue;
252
+ }
253
+
254
+ if (!entry.isFile()) continue;
255
+
256
+ const fullPath = join(dir, entry.name);
257
+ try {
258
+ const stats = statSync(fullPath);
259
+ const path = normalizePath(relative(rootDir, fullPath));
260
+ snapshot.set(path, {
261
+ path,
262
+ mtimeMs: stats.mtimeMs,
263
+ size: stats.size,
264
+ content: readTrackedFileContent(fullPath, stats.size),
265
+ });
266
+ } catch {
267
+ // Skip files we can't stat (deleted between readdir and stat, etc.)
268
+ }
269
+ }
270
+ }
271
+
272
+ walk(rootDir);
273
+ return snapshot;
274
+ }
275
+
276
+ export function diffWorkspaceSnapshots(
277
+ previous: Map<string, WorkspaceFileSnapshot>,
278
+ next: Map<string, WorkspaceFileSnapshot>,
279
+ detectedAt = Date.now(),
280
+ ): RunFileActivity[] {
281
+ const activity: RunFileActivity[] = [];
282
+ const allPaths = new Set([...previous.keys(), ...next.keys()]);
283
+
284
+ for (const path of [...allPaths].sort()) {
285
+ const before = previous.get(path);
286
+ const after = next.get(path);
287
+
288
+ if (!before && after) {
289
+ const diff = createTextDiffExcerpt("", after.content ?? "");
290
+ activity.push({
291
+ path,
292
+ operation: "created",
293
+ detectedAt,
294
+ addedLines: diff?.addedLines,
295
+ removedLines: diff?.removedLines,
296
+ diffLines: diff?.diffLines,
297
+ });
298
+ continue;
299
+ }
300
+
301
+ if (before && !after) {
302
+ const diff = createTextDiffExcerpt(before.content ?? "", "");
303
+ activity.push({
304
+ path,
305
+ operation: "deleted",
306
+ detectedAt,
307
+ addedLines: diff?.addedLines,
308
+ removedLines: diff?.removedLines,
309
+ diffLines: diff?.diffLines,
310
+ });
311
+ continue;
312
+ }
313
+
314
+ if (!before || !after) continue;
315
+
316
+ const metadataChanged = before.mtimeMs !== after.mtimeMs || before.size !== after.size;
317
+ const contentChanged = before.content !== after.content;
318
+ if (!metadataChanged && !contentChanged) continue;
319
+
320
+ const diff = before.content !== undefined && after.content !== undefined
321
+ ? createTextDiffExcerpt(before.content, after.content)
322
+ : undefined;
323
+
324
+ activity.push({
325
+ path,
326
+ operation: "modified",
327
+ detectedAt,
328
+ addedLines: diff?.addedLines,
329
+ removedLines: diff?.removedLines,
330
+ diffLines: diff?.diffLines,
331
+ });
332
+ }
333
+
334
+ return activity;
335
+ }
336
+
337
+ export function summarizeRunActivity(activity: RunFileActivity[]): RunActivitySummary | undefined {
338
+ if (activity.length === 0) return undefined;
339
+
340
+ let created = 0;
341
+ let modified = 0;
342
+ let deleted = 0;
343
+ for (const item of activity) {
344
+ if (item.operation === "created") created += 1;
345
+ if (item.operation === "modified") modified += 1;
346
+ if (item.operation === "deleted") deleted += 1;
347
+ }
348
+
349
+ return {
350
+ created,
351
+ modified,
352
+ deleted,
353
+ totalActivityCount: activity.length,
354
+ recent: activity.slice(-MAX_SUMMARY_ACTIVITY_ITEMS),
355
+ };
356
+ }
357
+
358
+ export function createWorkspaceActivityTracker(
359
+ options: WorkspaceActivityTrackerOptions,
360
+ ): WorkspaceActivityTracker {
361
+ let stopped = false;
362
+ let previous = options.initialSnapshot ?? captureWorkspaceSnapshot(options.rootDir);
363
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
364
+
365
+ const poll = () => {
366
+ if (stopped) return;
367
+ const next = captureWorkspaceSnapshot(options.rootDir);
368
+ const activity = diffWorkspaceSnapshots(previous, next);
369
+ previous = next;
370
+ if (activity.length > 0) {
371
+ options.onActivity(activity);
372
+ }
373
+ };
374
+
375
+ const timer = setInterval(poll, pollIntervalMs);
376
+ timer.unref?.();
377
+
378
+ return {
379
+ stop() {
380
+ stopped = true;
381
+ clearInterval(timer);
382
+ },
383
+ };
384
+ }