@myagentroam/node 0.1.4 → 0.1.7

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 (210) hide show
  1. package/README.md +9 -1
  2. package/dist/capabilities.d.ts +2 -0
  3. package/dist/capabilities.js +15 -4
  4. package/dist/claude-agent-sdk.d.ts +3 -1
  5. package/dist/claude-agent-sdk.js +9 -2
  6. package/dist/claude-channel.js +0 -1
  7. package/dist/codex-app-server.d.ts +1 -0
  8. package/dist/codex-app-server.js +8 -1
  9. package/dist/config.d.ts +3 -0
  10. package/dist/config.js +18 -2
  11. package/dist/connector/node-connector-options.d.ts +3 -0
  12. package/dist/connector/node-connector-options.js +0 -1
  13. package/dist/connector/node-operation-router.js +0 -1
  14. package/dist/connector.d.ts +3 -0
  15. package/dist/connector.js +109 -7
  16. package/dist/control-outbound-scheduler.js +0 -1
  17. package/dist/database.d.ts +4 -0
  18. package/dist/database.js +14 -9
  19. package/dist/event-buffer.js +0 -1
  20. package/dist/fake-runner.js +0 -1
  21. package/dist/health.js +0 -1
  22. package/dist/main.js +6 -1
  23. package/dist/migrations/v001.js +0 -1
  24. package/dist/migrations/v002.js +0 -1
  25. package/dist/migrations/v003.d.ts +4 -0
  26. package/dist/migrations/v003.js +44 -0
  27. package/dist/native-jsonl-reader.js +0 -1
  28. package/dist/native-session-history.d.ts +19 -1
  29. package/dist/native-session-history.js +65 -3
  30. package/dist/opencode-runtime.d.ts +33 -0
  31. package/dist/opencode-runtime.js +80 -0
  32. package/dist/opencode-server.d.ts +47 -0
  33. package/dist/opencode-server.js +250 -0
  34. package/dist/operational.js +10 -3
  35. package/dist/process-tree.js +0 -1
  36. package/dist/runner/abstract-runner.d.ts +7 -2
  37. package/dist/runner/abstract-runner.js +17 -2
  38. package/dist/runner/claude/managed-run-controller.js +20 -1
  39. package/dist/runner/claude-code-runner.d.ts +3 -1
  40. package/dist/runner/claude-code-runner.js +26 -8
  41. package/dist/runner/codex/conversation-parser.d.ts +1 -3
  42. package/dist/runner/codex/conversation-parser.js +174 -34
  43. package/dist/runner/codex/managed-run-controller.d.ts +2 -1
  44. package/dist/runner/codex/managed-run-controller.js +13 -4
  45. package/dist/runner/codex-runner.d.ts +4 -0
  46. package/dist/runner/codex-runner.js +27 -4
  47. package/dist/runner/fake-test-runner.js +0 -1
  48. package/dist/runner/opencode/conversation-parser.d.ts +27 -0
  49. package/dist/runner/opencode/conversation-parser.js +217 -0
  50. package/dist/runner/opencode/managed-run-controller.d.ts +76 -0
  51. package/dist/runner/opencode/managed-run-controller.js +451 -0
  52. package/dist/runner/opencode-runner.d.ts +60 -0
  53. package/dist/runner/opencode-runner.js +330 -0
  54. package/dist/runner/runner-registry.d.ts +3 -1
  55. package/dist/runner/runner-registry.js +12 -3
  56. package/dist/runner-command-engine.js +0 -1
  57. package/dist/runner-profiles.d.ts +4 -0
  58. package/dist/runner-profiles.js +76 -3
  59. package/dist/runner-usage.js +0 -1
  60. package/dist/runtime-state.js +20 -9
  61. package/dist/service/attachment-domain-state.js +0 -1
  62. package/dist/service/conversation-history-service.js +0 -1
  63. package/dist/service/external-session-resume-service.js +0 -1
  64. package/dist/service/fake-managed-run-service.js +0 -1
  65. package/dist/service/managed-runner-session-service.d.ts +3 -1
  66. package/dist/service/managed-runner-session-service.js +8 -4
  67. package/dist/service/native-session-projection-service.d.ts +2 -1
  68. package/dist/service/native-session-projection-service.js +8 -5
  69. package/dist/service/native-session-watch-service.js +0 -1
  70. package/dist/service/node-connection-lifecycle-service.js +3 -2
  71. package/dist/service/node-control-channel.d.ts +1 -1
  72. package/dist/service/node-control-channel.js +5 -4
  73. package/dist/service/node-control-message-service.js +0 -1
  74. package/dist/service/node-request-service.js +0 -1
  75. package/dist/service/node-run-event-bridge.d.ts +2 -0
  76. package/dist/service/node-run-event-bridge.js +4 -1
  77. package/dist/service/node-terminal-channel.js +16 -10
  78. package/dist/service/node-upgrade-coordinator.d.ts +29 -0
  79. package/dist/service/node-upgrade-coordinator.js +137 -0
  80. package/dist/service/node-workspace-coordinator.js +0 -1
  81. package/dist/service/run-attachment-service.js +0 -1
  82. package/dist/service/run-domain-state.js +0 -1
  83. package/dist/service/run-event-service.js +0 -1
  84. package/dist/service/run-workbench-service.js +0 -1
  85. package/dist/service/runner-channel-message-service.js +0 -1
  86. package/dist/service/runner-interaction-service.js +2 -1
  87. package/dist/service/runner-service.d.ts +5 -4
  88. package/dist/service/runner-service.js +19 -6
  89. package/dist/service/session-catalog-service.d.ts +16 -1
  90. package/dist/service/session-catalog-service.js +82 -3
  91. package/dist/service/session-command-service.js +0 -1
  92. package/dist/service/session-context-service.js +0 -1
  93. package/dist/service/session-domain-state.js +0 -1
  94. package/dist/service/session-identity-service.js +0 -1
  95. package/dist/service/session-lifecycle-service.js +13 -7
  96. package/dist/service/session-message-service.js +0 -1
  97. package/dist/service/session-presentation-service.js +0 -1
  98. package/dist/service/session-query-service.js +0 -1
  99. package/dist/service/terminal-relay-state.js +0 -1
  100. package/dist/service/terminal-service.js +0 -1
  101. package/dist/service/workbench-event-service.js +0 -1
  102. package/dist/service/workbench-manifest-service.d.ts +5 -4
  103. package/dist/service/workbench-manifest-service.js +3 -2
  104. package/dist/service/workspace-change-service.js +2 -2
  105. package/dist/service/workspace-content-search-service.d.ts +14 -0
  106. package/dist/service/workspace-content-search-service.js +477 -0
  107. package/dist/service/workspace-domain-state.js +0 -1
  108. package/dist/service/workspace-file-service.d.ts +3 -0
  109. package/dist/service/workspace-file-service.js +35 -3
  110. package/dist/service/workspace-queue-workbench-service.js +0 -1
  111. package/dist/service/workspace-service.js +23 -10
  112. package/dist/service/workspace-session-service.js +0 -1
  113. package/dist/service/workspace-upload-service.js +0 -1
  114. package/dist/service/workspace-watch-service.js +0 -1
  115. package/dist/service/workspace-workbench-service.d.ts +9 -0
  116. package/dist/service/workspace-workbench-service.js +69 -2
  117. package/dist/service.js +2 -3
  118. package/dist/storage.js +0 -1
  119. package/dist/supervisor.d.ts +1 -0
  120. package/dist/supervisor.js +154 -0
  121. package/dist/terminal.js +0 -1
  122. package/dist/util/node-operation-parsers.d.ts +1 -0
  123. package/dist/util/node-operation-parsers.js +3 -1
  124. package/dist/util/runner-native-session-parsers.js +117 -73
  125. package/dist/util/secret-environment.js +0 -1
  126. package/dist/workspace.d.ts +62 -2
  127. package/dist/workspace.js +539 -57
  128. package/package.json +3 -2
  129. package/dist/capabilities.js.map +0 -1
  130. package/dist/claude-agent-sdk.js.map +0 -1
  131. package/dist/claude-channel.js.map +0 -1
  132. package/dist/codex-app-server.js.map +0 -1
  133. package/dist/config.js.map +0 -1
  134. package/dist/connector/node-connector-options.js.map +0 -1
  135. package/dist/connector/node-operation-router.js.map +0 -1
  136. package/dist/connector.js.map +0 -1
  137. package/dist/control-outbound-scheduler.js.map +0 -1
  138. package/dist/database.js.map +0 -1
  139. package/dist/event-buffer.js.map +0 -1
  140. package/dist/fake-runner.js.map +0 -1
  141. package/dist/health.js.map +0 -1
  142. package/dist/main.js.map +0 -1
  143. package/dist/migrations/v001.js.map +0 -1
  144. package/dist/migrations/v002.js.map +0 -1
  145. package/dist/native-jsonl-reader.js.map +0 -1
  146. package/dist/native-session-history.js.map +0 -1
  147. package/dist/operational.js.map +0 -1
  148. package/dist/process-tree.js.map +0 -1
  149. package/dist/runner/abstract-runner.js.map +0 -1
  150. package/dist/runner/claude/managed-run-controller.js.map +0 -1
  151. package/dist/runner/claude-code-runner.js.map +0 -1
  152. package/dist/runner/codex/conversation-parser.js.map +0 -1
  153. package/dist/runner/codex/managed-run-controller.js.map +0 -1
  154. package/dist/runner/codex-runner.js.map +0 -1
  155. package/dist/runner/fake-test-runner.js.map +0 -1
  156. package/dist/runner/runner-registry.js.map +0 -1
  157. package/dist/runner-command-engine.js.map +0 -1
  158. package/dist/runner-profiles.js.map +0 -1
  159. package/dist/runner-usage.js.map +0 -1
  160. package/dist/runtime-state.js.map +0 -1
  161. package/dist/service/attachment-domain-state.js.map +0 -1
  162. package/dist/service/conversation-history-service.js.map +0 -1
  163. package/dist/service/external-session-resume-service.js.map +0 -1
  164. package/dist/service/fake-managed-run-service.js.map +0 -1
  165. package/dist/service/managed-runner-session-service.js.map +0 -1
  166. package/dist/service/native-session-projection-service.js.map +0 -1
  167. package/dist/service/native-session-watch-service.js.map +0 -1
  168. package/dist/service/node-connection-lifecycle-service.js.map +0 -1
  169. package/dist/service/node-control-channel.js.map +0 -1
  170. package/dist/service/node-control-message-service.js.map +0 -1
  171. package/dist/service/node-request-service.js.map +0 -1
  172. package/dist/service/node-run-event-bridge.js.map +0 -1
  173. package/dist/service/node-terminal-channel.js.map +0 -1
  174. package/dist/service/node-workspace-coordinator.js.map +0 -1
  175. package/dist/service/run-attachment-service.js.map +0 -1
  176. package/dist/service/run-domain-state.js.map +0 -1
  177. package/dist/service/run-event-service.js.map +0 -1
  178. package/dist/service/run-workbench-service.js.map +0 -1
  179. package/dist/service/runner-channel-message-service.js.map +0 -1
  180. package/dist/service/runner-interaction-service.js.map +0 -1
  181. package/dist/service/runner-service.js.map +0 -1
  182. package/dist/service/session-catalog-service.js.map +0 -1
  183. package/dist/service/session-command-service.js.map +0 -1
  184. package/dist/service/session-context-service.js.map +0 -1
  185. package/dist/service/session-domain-state.js.map +0 -1
  186. package/dist/service/session-identity-service.js.map +0 -1
  187. package/dist/service/session-lifecycle-service.js.map +0 -1
  188. package/dist/service/session-message-service.js.map +0 -1
  189. package/dist/service/session-presentation-service.js.map +0 -1
  190. package/dist/service/session-query-service.js.map +0 -1
  191. package/dist/service/terminal-relay-state.js.map +0 -1
  192. package/dist/service/terminal-service.js.map +0 -1
  193. package/dist/service/workbench-event-service.js.map +0 -1
  194. package/dist/service/workbench-manifest-service.js.map +0 -1
  195. package/dist/service/workspace-change-service.js.map +0 -1
  196. package/dist/service/workspace-domain-state.js.map +0 -1
  197. package/dist/service/workspace-file-service.js.map +0 -1
  198. package/dist/service/workspace-queue-workbench-service.js.map +0 -1
  199. package/dist/service/workspace-service.js.map +0 -1
  200. package/dist/service/workspace-session-service.js.map +0 -1
  201. package/dist/service/workspace-upload-service.js.map +0 -1
  202. package/dist/service/workspace-watch-service.js.map +0 -1
  203. package/dist/service/workspace-workbench-service.js.map +0 -1
  204. package/dist/service.js.map +0 -1
  205. package/dist/storage.js.map +0 -1
  206. package/dist/terminal.js.map +0 -1
  207. package/dist/util/node-operation-parsers.js.map +0 -1
  208. package/dist/util/runner-native-session-parsers.js.map +0 -1
  209. package/dist/util/secret-environment.js.map +0 -1
  210. package/dist/workspace.js.map +0 -1
package/dist/workspace.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { watch } from 'node:fs';
3
- import { lstat, readdir, readFile, realpath, stat, unlink } from 'node:fs/promises';
3
+ import { lstat, mkdir, opendir, readdir, readFile, realpath, rename, rm, stat, unlink, writeFile } from 'node:fs/promises';
4
4
  import { homedir, platform as hostPlatform } from 'node:os';
5
5
  import { dirname, isAbsolute, posix, relative, resolve, sep, win32 } from 'node:path';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { minimatch } from 'minimatch';
8
8
  const GIT_TIMEOUT_MS = 5_000;
9
9
  const GIT_OUTPUT_LIMIT = 512 * 1024;
10
+ const GIT_BINARY_PREVIEW_LIMIT = 4 * 1024 * 1024;
10
11
  const FILE_READ_LIMIT = 512 * 1024;
11
12
  const DIRECTORY_PAGE_LIMIT = 200;
12
13
  const DIRECTORY_PICKER_LIMIT = 200;
@@ -14,6 +15,7 @@ const SEARCH_RESULT_LIMIT = 200;
14
15
  const FILE_INDEX_IDLE_TTL_MS = 60 * 60 * 1_000;
15
16
  const FILE_INDEX_ENTRY_LIMIT = 100_000;
16
17
  const FILE_INDEX_FALLBACK_POLL_MS = 30_000;
18
+ const FILE_INDEX_METADATA_CONCURRENCY = 32;
17
19
  const DEFAULT_QUICK_OPEN_EXCLUDES = [
18
20
  '**/.git/**',
19
21
  '**/node_modules/**',
@@ -27,6 +29,7 @@ const DEFAULT_QUICK_OPEN_EXCLUDES = [
27
29
  '**/build/**',
28
30
  '**/out/**',
29
31
  '**/coverage/**',
32
+ '**/test/runs/**',
30
33
  '**/.next/**',
31
34
  '**/.nuxt/**',
32
35
  '**/.svelte-kit/**',
@@ -41,6 +44,36 @@ const DEFAULT_QUICK_OPEN_EXCLUDES = [
41
44
  '**/venv/**'
42
45
  ];
43
46
  const REF_PATTERN = /^(?!-)(?!.*\.\.)(?!.*@\{)[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/;
47
+ export async function createWorkspaceDirectory(parentPath, directoryName, allowedRoots) {
48
+ const name = directoryName.trim();
49
+ if (name.length === 0 ||
50
+ name.length > 255 ||
51
+ name === '.' ||
52
+ name === '..' ||
53
+ name.includes('/') ||
54
+ name.includes('\\') ||
55
+ name.includes('\0'))
56
+ throw new Error('WORKSPACE_DIRECTORY_NAME_INVALID');
57
+ const roots = await Promise.all(allowedRoots.map((root) => realpath(root)));
58
+ const parent = await realpath(parentPath).catch(() => {
59
+ throw new Error('WORKSPACE_DIRECTORY_NOT_ALLOWED');
60
+ });
61
+ const metadata = await stat(parent).catch(() => undefined);
62
+ if (metadata?.isDirectory() !== true || !isWithinAllowedRoot(parent, roots))
63
+ throw new Error('WORKSPACE_DIRECTORY_NOT_ALLOWED');
64
+ const target = resolve(parent, name);
65
+ if (dirname(target) !== parent || !isWithinAllowedRoot(target, roots))
66
+ throw new Error('WORKSPACE_DIRECTORY_NAME_INVALID');
67
+ try {
68
+ await mkdir(target);
69
+ }
70
+ catch (error) {
71
+ if (error.code === 'EEXIST')
72
+ throw new Error('WORKSPACE_DIRECTORY_EXISTS');
73
+ throw error;
74
+ }
75
+ return target;
76
+ }
44
77
  /** A per-workspace, process-local index for the Quick Open file picker. */
45
78
  export class WorkspaceFileIndex {
46
79
  workspaceRoot;
@@ -63,6 +96,14 @@ export class WorkspaceFileIndex {
63
96
  await this.ensureLoaded();
64
97
  this.touch();
65
98
  }
99
+ async contentCandidates() {
100
+ await this.ensureLoaded();
101
+ this.touch();
102
+ return {
103
+ entries: this.entries.filter((entry) => entry.kind === 'FILE'),
104
+ truncated: this.truncated
105
+ };
106
+ }
66
107
  dispose() {
67
108
  if (this.expiryTimer !== undefined)
68
109
  clearTimeout(this.expiryTimer);
@@ -77,7 +118,7 @@ export class WorkspaceFileIndex {
77
118
  this.stale = true;
78
119
  this.signature = '';
79
120
  }
80
- async search(query, cursor, limit = SEARCH_RESULT_LIMIT) {
121
+ async search(query, cursor, limit = SEARCH_RESULT_LIMIT, includeDirectories = false) {
81
122
  if (query.trim().length === 0 ||
82
123
  query.length > 256 ||
83
124
  !Number.isSafeInteger(limit) ||
@@ -87,15 +128,28 @@ export class WorkspaceFileIndex {
87
128
  await this.ensureLoaded();
88
129
  this.touch();
89
130
  const after = decodeSearchCursor(cursor, query);
90
- const ranked = this.entries
91
- .map((entry) => ({ entry, score: fuzzyPathScore(entry.path, query) }))
92
- .filter((candidate) => candidate.score !== -1)
93
- .sort((left, right) => right.score - left.score || left.entry.path.localeCompare(right.entry.path));
94
- const start = after === undefined ? 0 : ranked.findIndex((candidate) => candidate.entry.path === after) + 1;
95
- const page = ranked
96
- .slice(Math.max(0, start), Math.max(0, start) + limit)
97
- .map((candidate) => candidate.entry);
98
- const next = ranked[Math.max(0, start) + limit]?.entry.path;
131
+ const afterEntry = after === undefined
132
+ ? undefined
133
+ : this.entries.find((entry) => entry.path === after && (includeDirectories || entry.kind === 'FILE'));
134
+ const afterScore = afterEntry === undefined ? -1 : fuzzyPathScore(afterEntry.path, query);
135
+ const afterCandidate = afterEntry === undefined || afterScore < 0
136
+ ? undefined
137
+ : { entry: afterEntry, score: afterScore };
138
+ const ranked = new RankedCandidateHeap(limit + 1);
139
+ for (const entry of this.entries) {
140
+ if (!includeDirectories && entry.kind !== 'FILE')
141
+ continue;
142
+ const score = fuzzyPathScore(entry.path, query);
143
+ if (score < 0)
144
+ continue;
145
+ const candidate = { entry, score };
146
+ if (afterCandidate !== undefined && compareRankedCandidate(candidate, afterCandidate) <= 0)
147
+ continue;
148
+ ranked.add(candidate);
149
+ }
150
+ const candidates = ranked.sorted();
151
+ const page = candidates.slice(0, limit).map((candidate) => candidate.entry);
152
+ const next = candidates[limit]?.entry.path;
99
153
  return {
100
154
  entries: page,
101
155
  nextCursor: next === undefined ? null : encodeSearchCursor(query, page.at(-1)?.path ?? after ?? ''),
@@ -125,43 +179,52 @@ export class WorkspaceFileIndex {
125
179
  const entryLimit = this.limits.entryLimit ?? FILE_INDEX_ENTRY_LIMIT;
126
180
  let truncated = false;
127
181
  const directories = ['.'];
128
- while (directories.length > 0 && !truncated) {
129
- const relativeDirectory = directories.shift();
130
- const directory = await resolveWorkspaceEntry(root, relativeDirectory, true).catch(() => undefined);
182
+ let directoryCursor = 0;
183
+ while (directoryCursor < directories.length && !truncated) {
184
+ const relativeDirectory = directories[directoryCursor];
185
+ directoryCursor += 1;
186
+ const directory = await resolveWorkspaceEntryFromCanonicalRoot(root, relativeDirectory, true).catch(() => undefined);
131
187
  if (directory === undefined)
132
188
  continue;
133
- const children = await readdir(directory, { withFileTypes: true }).catch(() => undefined);
134
- if (children === undefined)
189
+ const stream = await opendir(directory).catch(() => undefined);
190
+ if (stream === undefined)
135
191
  continue;
136
- for (const child of children) {
137
- const candidatePath = relative(root, resolve(directory, child.name)).split(sep).join('/');
192
+ let pending = [];
193
+ const appendPending = async () => {
194
+ const resolvedEntries = await Promise.all(pending);
195
+ pending = [];
196
+ for (const entry of resolvedEntries) {
197
+ if (entry === undefined)
198
+ continue;
199
+ if (entry.path === '.git' ||
200
+ entry.path.startsWith('.git/') ||
201
+ isExcluded(entry.path, excludes))
202
+ continue;
203
+ if (entry.kind === 'DIRECTORY')
204
+ directories.push(entry.path);
205
+ indexed.push(entry);
206
+ if (indexed.length >= entryLimit) {
207
+ truncated = true;
208
+ break;
209
+ }
210
+ }
211
+ };
212
+ for await (const child of stream) {
213
+ const candidatePath = relativeDirectory === '.'
214
+ ? child.name
215
+ : posix.join(relativeDirectory.replaceAll('\\', '/'), child.name);
138
216
  if (candidatePath === '.git' ||
139
217
  candidatePath.startsWith('.git/') ||
140
218
  isExcluded(candidatePath, excludes))
141
219
  continue;
142
- const resolved = await resolveWorkspaceEntry(root, candidatePath, false).catch(() => undefined);
143
- if (resolved === undefined)
144
- continue;
145
- const metadata = await lstat(resolved).catch(() => undefined);
146
- if (metadata === undefined || (!metadata.isFile() && !metadata.isDirectory()))
147
- continue;
148
- const safePath = relative(root, resolved).split(sep).join('/');
149
- if (metadata.isDirectory()) {
150
- directories.push(safePath);
151
- continue;
152
- }
153
- indexed.push({
154
- path: safePath,
155
- name: child.name,
156
- kind: 'FILE',
157
- size: metadata.size,
158
- modifiedAt: metadata.mtimeMs
159
- });
160
- if (indexed.length >= entryLimit) {
161
- truncated = true;
220
+ pending.push(inspectWorkspaceIndexEntry(root, candidatePath, child.name));
221
+ if (pending.length >= FILE_INDEX_METADATA_CONCURRENCY)
222
+ await appendPending();
223
+ if (truncated)
162
224
  break;
163
- }
164
225
  }
226
+ if (!truncated && pending.length > 0)
227
+ await appendPending();
165
228
  }
166
229
  this.root = root;
167
230
  this.entries = indexed;
@@ -172,7 +235,12 @@ export class WorkspaceFileIndex {
172
235
  // fs.watch is advisory. Any change safely invalidates the in-memory index;
173
236
  // the next lookup rebuilds it and never trusts stale paths for file reads.
174
237
  try {
175
- this.watcher = watch(root, { persistent: false, recursive: true }, () => {
238
+ this.watcher = watch(root, { persistent: false, recursive: true }, (_event, filename) => {
239
+ if (filename !== null) {
240
+ const path = filename.toString().replaceAll('\\', '/');
241
+ if (path === '.git' || path.startsWith('.git/') || isExcluded(path, excludes))
242
+ return;
243
+ }
176
244
  this.invalidate();
177
245
  });
178
246
  this.watcher.on('error', () => {
@@ -219,10 +287,11 @@ export class WorkspaceFileIndex {
219
287
  }
220
288
  }
221
289
  function fileIndexSignature(entries, truncated) {
222
- return JSON.stringify({
223
- truncated,
224
- entries: entries.map((entry) => [entry.path, entry.size, entry.modifiedAt])
225
- });
290
+ const hash = createHash('sha256');
291
+ hash.update(truncated ? '1\0' : '0\0');
292
+ for (const entry of entries)
293
+ hash.update(`${entry.path}\0${String(entry.size)}\0${String(entry.modifiedAt)}\0`);
294
+ return hash.digest('hex');
226
295
  }
227
296
  async function workspaceExcludePatterns(root) {
228
297
  const settingsPath = resolve(root, '.vscode/settings.json');
@@ -242,6 +311,64 @@ function isExcluded(path, patterns) {
242
311
  return patterns.some((pattern) => minimatch(path, pattern, { dot: true, nocase: hostPlatform() !== 'linux' }) ||
243
312
  minimatch(`${path}/`, pattern, { dot: true, nocase: hostPlatform() !== 'linux' }));
244
313
  }
314
+ function compareRankedCandidate(left, right) {
315
+ return right.score - left.score || left.entry.path.localeCompare(right.entry.path);
316
+ }
317
+ /** Keeps only the best page-sized candidate set, with the current worst item at the root. */
318
+ class RankedCandidateHeap {
319
+ capacity;
320
+ values = [];
321
+ constructor(capacity) {
322
+ this.capacity = capacity;
323
+ }
324
+ add(candidate) {
325
+ if (this.values.length < this.capacity) {
326
+ this.values.push(candidate);
327
+ this.bubbleUp(this.values.length - 1);
328
+ return;
329
+ }
330
+ if (compareRankedCandidate(candidate, this.values[0]) >= 0)
331
+ return;
332
+ this.values[0] = candidate;
333
+ this.sinkDown(0);
334
+ }
335
+ sorted() {
336
+ return [...this.values].sort(compareRankedCandidate);
337
+ }
338
+ bubbleUp(start) {
339
+ let index = start;
340
+ while (index > 0) {
341
+ const parent = Math.floor((index - 1) / 2);
342
+ if (compareRankedCandidate(this.values[index], this.values[parent]) <= 0)
343
+ break;
344
+ [this.values[index], this.values[parent]] = [
345
+ this.values[parent],
346
+ this.values[index]
347
+ ];
348
+ index = parent;
349
+ }
350
+ }
351
+ sinkDown(start) {
352
+ let index = start;
353
+ while (true) {
354
+ const left = index * 2 + 1;
355
+ if (left >= this.values.length)
356
+ return;
357
+ const right = left + 1;
358
+ const worse = right < this.values.length &&
359
+ compareRankedCandidate(this.values[right], this.values[left]) > 0
360
+ ? right
361
+ : left;
362
+ if (compareRankedCandidate(this.values[worse], this.values[index]) <= 0)
363
+ return;
364
+ [this.values[index], this.values[worse]] = [
365
+ this.values[worse],
366
+ this.values[index]
367
+ ];
368
+ index = worse;
369
+ }
370
+ }
371
+ }
245
372
  function fuzzyPathScore(path, query) {
246
373
  const source = path.toLocaleLowerCase();
247
374
  const needle = query.trim().toLocaleLowerCase();
@@ -352,6 +479,98 @@ export async function listWorkspaceFiles(workspaceRoot, requestedPath = '.', cur
352
479
  truncated: nextName !== undefined
353
480
  };
354
481
  }
482
+ export async function mutateWorkspaceFile(workspaceRoot, mutation) {
483
+ const root = await realpath(workspaceRoot);
484
+ if (mutation.action === 'CREATE_FILE' || mutation.action === 'CREATE_DIRECTORY') {
485
+ const target = await resolveNewWorkspaceEntry(root, mutation.path);
486
+ if (mutation.action === 'CREATE_FILE')
487
+ await writeFile(target, '', { flag: 'wx' });
488
+ else
489
+ await mkdir(target);
490
+ return { path: workspaceRelativePath(root, target) };
491
+ }
492
+ await assertWorkspaceMutationPathHasNoSymlinks(root, mutation.path);
493
+ const source = await resolveWorkspaceEntryMetadata(root, mutation.path);
494
+ if (workspaceRelativePath(root, source.path) === '.')
495
+ throw new Error('FILE_ROOT_MUTATION_INVALID');
496
+ if (mutation.action === 'DELETE') {
497
+ await rm(source.path, { recursive: source.metadata.isDirectory(), force: false });
498
+ return { deleted: true };
499
+ }
500
+ const destination = await resolveNewWorkspaceEntry(root, mutation.destination);
501
+ if (source.metadata.isDirectory()) {
502
+ const destinationFromSource = relative(source.path, destination);
503
+ if (destinationFromSource === '' ||
504
+ (!isAbsolute(destinationFromSource) &&
505
+ destinationFromSource !== '..' &&
506
+ !destinationFromSource.startsWith(`..${sep}`)))
507
+ throw new Error('FILE_MOVE_DESCENDANT_INVALID');
508
+ }
509
+ await rename(source.path, destination);
510
+ return { path: workspaceRelativePath(root, destination) };
511
+ }
512
+ async function resolveNewWorkspaceEntry(root, requestedPath) {
513
+ const normalized = requestedPath.replaceAll('\\', '/');
514
+ const segments = normalized.split('/');
515
+ const name = segments.at(-1);
516
+ if (requestedPath.includes('\0') ||
517
+ normalized.startsWith('/') ||
518
+ isAbsolute(requestedPath) ||
519
+ win32.isAbsolute(normalized) ||
520
+ name === undefined ||
521
+ name.length === 0 ||
522
+ segments.some(invalidWorkspaceMutationSegment))
523
+ throw new Error('FILE_PATH_ESCAPE');
524
+ const parentPath = segments.length === 1 ? '.' : segments.slice(0, -1).join('/');
525
+ await assertWorkspaceMutationPathHasNoSymlinks(root, parentPath);
526
+ const parent = await resolveWorkspaceEntryFromCanonicalRoot(root, parentPath, true);
527
+ const target = resolve(parent, name);
528
+ try {
529
+ await lstat(target);
530
+ throw new Error('FILE_ALREADY_EXISTS');
531
+ }
532
+ catch (error) {
533
+ if (error.code !== 'ENOENT')
534
+ throw error;
535
+ }
536
+ return target;
537
+ }
538
+ async function assertWorkspaceMutationPathHasNoSymlinks(root, requestedPath) {
539
+ const normalized = requestedPath.replaceAll('\\', '/');
540
+ if (normalized === '.')
541
+ return;
542
+ const segments = normalized.split('/');
543
+ if (requestedPath.includes('\0') ||
544
+ normalized.startsWith('/') ||
545
+ isAbsolute(requestedPath) ||
546
+ win32.isAbsolute(normalized) ||
547
+ segments.some(invalidWorkspaceMutationSegment))
548
+ throw new Error('FILE_PATH_ESCAPE');
549
+ let candidate = root;
550
+ for (const segment of segments) {
551
+ candidate = resolve(candidate, segment);
552
+ const metadata = await lstat(candidate).catch((error) => {
553
+ if (error.code === 'ENOENT')
554
+ throw new Error('FILE_NOT_FOUND');
555
+ throw error;
556
+ });
557
+ if (metadata.isSymbolicLink())
558
+ throw new Error('FILE_PATH_ESCAPE');
559
+ }
560
+ }
561
+ function invalidWorkspaceMutationSegment(segment) {
562
+ if (segment.length === 0 || segment === '.' || segment === '..' || segment === '.git')
563
+ return true;
564
+ if (hostPlatform() !== 'win32')
565
+ return false;
566
+ const normalized = segment.replace(/[. ]+$/u, '').toUpperCase();
567
+ return (/[<>:"|?*]/u.test(segment) ||
568
+ /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\..*)?$/u.test(normalized));
569
+ }
570
+ function workspaceRelativePath(root, path) {
571
+ const value = relative(root, path).split(sep).join('/');
572
+ return value.length === 0 ? '.' : value;
573
+ }
355
574
  function isSkippableWorkspaceEntryError(error) {
356
575
  return (error instanceof Error &&
357
576
  (error.message === 'FILE_PATH_ESCAPE' ||
@@ -518,16 +737,28 @@ function isUtf8ContinuationByte(value) {
518
737
  return (value & 0b1100_0000) === 0b1000_0000;
519
738
  }
520
739
  /** Searches file names only, so an exploratory request cannot exfiltrate file contents. */
521
- export async function searchWorkspaceFiles(workspaceRoot, query, cursor, limit = SEARCH_RESULT_LIMIT) {
740
+ export async function searchWorkspaceFiles(workspaceRoot, query, cursor, limit = SEARCH_RESULT_LIMIT, includeDirectories = false) {
522
741
  const index = new WorkspaceFileIndex(workspaceRoot);
523
742
  try {
524
- return await index.search(query, cursor, limit);
743
+ return await index.search(query, cursor, limit, includeDirectories);
525
744
  }
526
745
  finally {
527
746
  index.dispose();
528
747
  }
529
748
  }
530
749
  async function resolveWorkspaceEntry(workspaceRoot, requestedPath, directory) {
750
+ const root = await realpath(workspaceRoot);
751
+ return resolveWorkspaceEntryFromCanonicalRoot(root, requestedPath, directory);
752
+ }
753
+ async function resolveWorkspaceEntryFromCanonicalRoot(root, requestedPath, directory) {
754
+ const entry = await resolveWorkspaceEntryMetadata(root, requestedPath);
755
+ if (directory
756
+ ? !entry.metadata.isDirectory()
757
+ : !entry.metadata.isFile() && !entry.metadata.isDirectory())
758
+ throw new Error(directory ? 'FILE_NOT_DIRECTORY' : 'FILE_NOT_REGULAR');
759
+ return entry.path;
760
+ }
761
+ async function resolveWorkspaceEntryMetadata(root, requestedPath) {
531
762
  if (requestedPath.includes('\0') ||
532
763
  requestedPath.length === 0 ||
533
764
  requestedPath.startsWith('/') ||
@@ -536,7 +767,6 @@ async function resolveWorkspaceEntry(workspaceRoot, requestedPath, directory) {
536
767
  const normalized = requestedPath.replaceAll('\\', '/');
537
768
  if (normalized.split('/').some((segment) => segment === '..' || segment === '.git'))
538
769
  throw new Error('FILE_PATH_ESCAPE');
539
- const root = await realpath(workspaceRoot);
540
770
  let candidate;
541
771
  try {
542
772
  candidate = await realpath(resolve(root, normalized));
@@ -547,14 +777,39 @@ async function resolveWorkspaceEntry(workspaceRoot, requestedPath, directory) {
547
777
  throw error;
548
778
  }
549
779
  const pathToCandidate = relative(root, candidate);
550
- if (pathToCandidate.startsWith('..') ||
780
+ if (isAbsolute(pathToCandidate) ||
781
+ pathToCandidate.startsWith('..') ||
551
782
  pathToCandidate === '..' ||
552
783
  pathToCandidate.startsWith(`..${sep}`))
553
784
  throw new Error('FILE_PATH_ESCAPE');
554
785
  const metadata = await lstat(candidate);
555
- if (directory ? !metadata.isDirectory() : !metadata.isFile() && !metadata.isDirectory())
556
- throw new Error(directory ? 'FILE_NOT_DIRECTORY' : 'FILE_NOT_REGULAR');
557
- return candidate;
786
+ return { path: candidate, metadata };
787
+ }
788
+ async function inspectWorkspaceIndexEntry(root, requestedPath, name) {
789
+ try {
790
+ const entry = await resolveWorkspaceEntryMetadata(root, requestedPath);
791
+ if (!entry.metadata.isFile() && !entry.metadata.isDirectory())
792
+ return undefined;
793
+ const safePath = relative(root, entry.path).split(sep).join('/');
794
+ return entry.metadata.isDirectory()
795
+ ? {
796
+ path: safePath,
797
+ name,
798
+ kind: 'DIRECTORY',
799
+ size: null,
800
+ modifiedAt: entry.metadata.mtimeMs
801
+ }
802
+ : {
803
+ path: safePath,
804
+ name,
805
+ kind: 'FILE',
806
+ size: entry.metadata.size,
807
+ modifiedAt: entry.metadata.mtimeMs
808
+ };
809
+ }
810
+ catch {
811
+ return undefined;
812
+ }
558
813
  }
559
814
  function encodeDirectoryCursor(path, name) {
560
815
  return Buffer.from(JSON.stringify({ path, name })).toString('base64url');
@@ -776,6 +1031,201 @@ export async function resolveGitRepository(workspacePath, repositoryPath) {
776
1031
  }
777
1032
  return repository;
778
1033
  }
1034
+ export async function listGitHistoryRefs(repository) {
1035
+ const result = await runGit(repository, [
1036
+ 'for-each-ref',
1037
+ '--format=%(refname)%09%(refname:short)%09%(objectname)%09%(HEAD)%09%(symref)',
1038
+ 'refs/heads',
1039
+ 'refs/remotes'
1040
+ ]);
1041
+ return result.text
1042
+ .split(/\r?\n/)
1043
+ .flatMap((line) => {
1044
+ const [name, displayName, hash, head, symref] = line.split('\t');
1045
+ if (name === undefined ||
1046
+ displayName === undefined ||
1047
+ hash === undefined ||
1048
+ symref === undefined ||
1049
+ symref.length > 0 ||
1050
+ !/^[0-9a-f]{40,64}$/.test(hash))
1051
+ return [];
1052
+ const kind = name.startsWith('refs/heads/')
1053
+ ? 'LOCAL'
1054
+ : name.startsWith('refs/remotes/')
1055
+ ? 'REMOTE'
1056
+ : undefined;
1057
+ return kind === undefined ? [] : [{ name, displayName, kind, hash, current: head === '*' }];
1058
+ })
1059
+ .sort((left, right) => left.kind.localeCompare(right.kind) || left.displayName.localeCompare(right.displayName));
1060
+ }
1061
+ function selectedHistoryRefs(available, requested) {
1062
+ if (requested === undefined || requested.length === 0)
1063
+ return available;
1064
+ const unique = [...new Set(requested)];
1065
+ const selected = unique.map((name) => available.find((entry) => entry.name === name));
1066
+ if (selected.some((entry) => entry === undefined))
1067
+ throw new Error('GIT_HISTORY_REF_INVALID');
1068
+ return selected;
1069
+ }
1070
+ function historyCursor(offset, fingerprint) {
1071
+ return Buffer.from(JSON.stringify({ offset, fingerprint })).toString('base64url');
1072
+ }
1073
+ function parseHistoryCursor(value, fingerprint) {
1074
+ if (value === undefined)
1075
+ return 0;
1076
+ try {
1077
+ const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
1078
+ if (!Number.isInteger(parsed.offset) ||
1079
+ parsed.offset < 0 ||
1080
+ parsed.fingerprint !== fingerprint)
1081
+ throw new Error('invalid');
1082
+ return parsed.offset;
1083
+ }
1084
+ catch {
1085
+ throw new Error('GIT_HISTORY_CURSOR_INVALID');
1086
+ }
1087
+ }
1088
+ export async function readGitHistory(repository, input) {
1089
+ const available = await listGitHistoryRefs(repository);
1090
+ const selected = selectedHistoryRefs(available, input.refs);
1091
+ if (selected.length === 0)
1092
+ return { commits: [], nextCursor: null };
1093
+ const limit = input.limit ?? 50;
1094
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100)
1095
+ throw new Error('GIT_HISTORY_LIMIT_INVALID');
1096
+ const names = selected.map((entry) => entry.name).sort();
1097
+ // Bind the offset cursor to the exact branch tips. Advancing a branch invalidates
1098
+ // the cursor instead of silently skipping or duplicating commits between pages.
1099
+ const fingerprint = createHash('sha256')
1100
+ .update(selected
1101
+ .map((entry) => `${entry.name}\0${entry.hash}`)
1102
+ .sort()
1103
+ .join('\0'))
1104
+ .digest('hex');
1105
+ const offset = parseHistoryCursor(input.cursor, fingerprint);
1106
+ const result = await runGit(repository, [
1107
+ 'log',
1108
+ '--topo-order',
1109
+ '--date-order',
1110
+ `--skip=${offset}`,
1111
+ `--max-count=${limit + 1}`,
1112
+ '-z',
1113
+ '--format=%H%x00%P%x00%an%x00%ae%x00%ct%x00%s',
1114
+ ...names,
1115
+ '--'
1116
+ ]);
1117
+ const refsByHash = new Map();
1118
+ for (const entry of available) {
1119
+ const refs = refsByHash.get(entry.hash) ?? [];
1120
+ refs.push(entry.displayName);
1121
+ refsByHash.set(entry.hash, refs);
1122
+ }
1123
+ const fields = result.text.split('\0');
1124
+ const commits = [];
1125
+ for (let index = 0; index + 5 < fields.length; index += 6) {
1126
+ const [hash, parents = '', authorName = '', authorEmail = '', committedAt = '0', subject = ''] = fields.slice(index, index + 6);
1127
+ if (hash === undefined || !/^[0-9a-f]{40,64}$/.test(hash))
1128
+ continue;
1129
+ commits.push({
1130
+ hash,
1131
+ parents: parents.length === 0 ? [] : parents.split(' '),
1132
+ authorName,
1133
+ authorEmail,
1134
+ committedAt: Number(committedAt) * 1000,
1135
+ subject,
1136
+ refs: refsByHash.get(hash) ?? []
1137
+ });
1138
+ }
1139
+ const hasMore = commits.length > limit;
1140
+ return {
1141
+ commits: commits.slice(0, limit),
1142
+ nextCursor: hasMore ? historyCursor(offset + limit, fingerprint) : null
1143
+ };
1144
+ }
1145
+ async function assertHistoryCommitReachable(repository, commit, refs) {
1146
+ if (!/^[0-9a-f]{40,64}$/.test(commit))
1147
+ throw new Error('GIT_HISTORY_COMMIT_INVALID');
1148
+ const selected = selectedHistoryRefs(await listGitHistoryRefs(repository), refs);
1149
+ for (const ref of selected) {
1150
+ const result = await runGitAllowFailure(repository, [
1151
+ 'merge-base',
1152
+ '--is-ancestor',
1153
+ commit,
1154
+ ref.name
1155
+ ]);
1156
+ if (result.code === 0)
1157
+ return;
1158
+ }
1159
+ throw new Error('GIT_HISTORY_COMMIT_INVALID');
1160
+ }
1161
+ export async function readGitHistoryFiles(repository, commit, refs) {
1162
+ await assertHistoryCommitReachable(repository, commit, refs);
1163
+ const result = await runGit(repository, [
1164
+ 'diff-tree',
1165
+ '--root',
1166
+ '--first-parent',
1167
+ '--no-commit-id',
1168
+ '-r',
1169
+ '-z',
1170
+ '-M',
1171
+ '-C',
1172
+ '--name-status',
1173
+ commit
1174
+ ]);
1175
+ const records = result.text.split('\0');
1176
+ const files = [];
1177
+ for (let index = 0; index < records.length; index += 1) {
1178
+ const status = records[index];
1179
+ if (status === undefined || status.length === 0)
1180
+ continue;
1181
+ const code = status[0] ?? 'M';
1182
+ if (code === 'R' || code === 'C') {
1183
+ const previousPath = records[index + 1];
1184
+ const path = records[index + 2];
1185
+ if (previousPath !== undefined && path !== undefined)
1186
+ files.push({ path, previousPath, change: gitChange(code) });
1187
+ index += 2;
1188
+ }
1189
+ else {
1190
+ const path = records[index + 1];
1191
+ if (path !== undefined)
1192
+ files.push({ path, change: gitChange(code) });
1193
+ index += 1;
1194
+ }
1195
+ }
1196
+ return files;
1197
+ }
1198
+ export async function readGitHistoryFileDiff(repository, commit, requestedPath, refs, requestedPreviousPath) {
1199
+ await assertHistoryCommitReachable(repository, commit, refs);
1200
+ const path = safeGitWorkspacePath(requestedPath);
1201
+ const previousPath = requestedPreviousPath === undefined ? undefined : safeGitWorkspacePath(requestedPreviousPath);
1202
+ const result = await runGit(repository, [
1203
+ 'show',
1204
+ '--format=',
1205
+ '--first-parent',
1206
+ '--no-ext-diff',
1207
+ '--no-textconv',
1208
+ commit,
1209
+ '--',
1210
+ `:(literal)${path}`,
1211
+ ...(previousPath === undefined || previousPath === path ? [] : [`:(literal)${previousPath}`])
1212
+ ]);
1213
+ const binary = result.output.includes(0) || result.text.includes('Binary files ');
1214
+ if (binary) {
1215
+ const [beforePreview, afterPreview] = await Promise.all([
1216
+ readGitBinaryPreview(repository, `${commit}^`, previousPath ?? path),
1217
+ readGitBinaryPreview(repository, commit, path)
1218
+ ]);
1219
+ return {
1220
+ binary: true,
1221
+ summary: `binary diff sha256=${createHash('sha256').update(result.output).digest('hex')} bytes=${result.output.length}`,
1222
+ truncated: result.truncated,
1223
+ ...(beforePreview === undefined ? {} : { beforePreview }),
1224
+ ...(afterPreview === undefined ? {} : { afterPreview })
1225
+ };
1226
+ }
1227
+ return { binary: false, text: result.text, truncated: result.truncated };
1228
+ }
779
1229
  export async function readCurrentChanges(workspacePath) {
780
1230
  return (await readCurrentChangesSummary(workspacePath)).changes;
781
1231
  }
@@ -793,10 +1243,16 @@ export async function readCurrentChangeDiff(workspacePath, requestedPath) {
793
1243
  const after = await readWorkingTreeText(workspacePath, path);
794
1244
  const binary = result.output.includes(0) || result.text.includes('Binary files ') || after?.binary === true;
795
1245
  if (binary) {
1246
+ const [beforePreview, afterPreview] = await Promise.all([
1247
+ readGitBinaryPreview(workspacePath, 'HEAD', path),
1248
+ readWorkingTreeBinaryPreview(workspacePath, path)
1249
+ ]);
796
1250
  return {
797
1251
  binary: true,
798
1252
  summary: `binary diff sha256=${createHash('sha256').update(result.output).digest('hex')} bytes=${result.output.length}`,
799
- truncated: result.truncated || after?.truncated === true
1253
+ truncated: result.truncated || after?.truncated === true,
1254
+ ...(beforePreview === undefined ? {} : { beforePreview }),
1255
+ ...(afterPreview === undefined ? {} : { afterPreview })
800
1256
  };
801
1257
  }
802
1258
  return {
@@ -834,6 +1290,32 @@ async function assertCurrentChangePathAccessible(workspacePath, path) {
834
1290
  return;
835
1291
  await resolveWorkspaceEntry(root, path, false);
836
1292
  }
1293
+ async function readGitBinaryPreview(repository, ref, path) {
1294
+ const result = await runGitAllowFailure(repository, ['show', `${ref}:${path}`], GIT_BINARY_PREVIEW_LIMIT);
1295
+ if (result.code !== 0)
1296
+ return undefined;
1297
+ return result.truncated
1298
+ ? { truncated: true }
1299
+ : { contentBase64: result.output.toString('base64'), truncated: false };
1300
+ }
1301
+ async function readWorkingTreeBinaryPreview(workspacePath, path) {
1302
+ const root = await realpath(workspacePath);
1303
+ let file;
1304
+ try {
1305
+ file = await resolveWorkspaceEntry(root, path, false);
1306
+ }
1307
+ catch (error) {
1308
+ if (error instanceof Error && error.message === 'FILE_NOT_FOUND')
1309
+ return undefined;
1310
+ throw error;
1311
+ }
1312
+ const metadata = await lstat(file);
1313
+ if (!metadata.isFile())
1314
+ return undefined;
1315
+ if (metadata.size > GIT_BINARY_PREVIEW_LIMIT)
1316
+ return { truncated: true };
1317
+ return { contentBase64: (await readFile(file)).toString('base64'), truncated: false };
1318
+ }
837
1319
  async function readWorkingTreeText(workspacePath, path) {
838
1320
  const root = await realpath(workspacePath);
839
1321
  let file;
@@ -938,7 +1420,7 @@ async function runGit(workspacePath, args) {
938
1420
  }
939
1421
  return result;
940
1422
  }
941
- async function runGitAllowFailure(workspacePath, args) {
1423
+ async function runGitAllowFailure(workspacePath, args, outputLimit = GIT_OUTPUT_LIMIT) {
942
1424
  return new Promise((resolve, reject) => {
943
1425
  const child = spawn('git', [...args], {
944
1426
  cwd: workspacePath,
@@ -952,12 +1434,13 @@ async function runGitAllowFailure(workspacePath, args) {
952
1434
  let truncated = false;
953
1435
  const timer = setTimeout(() => child.kill('SIGKILL'), GIT_TIMEOUT_MS);
954
1436
  child.stdout.on('data', (chunk) => {
955
- if (size < GIT_OUTPUT_LIMIT) {
956
- const retained = chunk.subarray(0, Math.max(0, GIT_OUTPUT_LIMIT - size));
1437
+ const remaining = Math.max(0, outputLimit - size);
1438
+ if (remaining > 0) {
1439
+ const retained = chunk.subarray(0, remaining);
957
1440
  chunks.push(retained);
958
1441
  size += retained.length;
959
1442
  }
960
- truncated ||= size >= GIT_OUTPUT_LIMIT;
1443
+ truncated ||= chunk.length > remaining;
961
1444
  });
962
1445
  child.stderr.on('data', (chunk) => errors.push(chunk.subarray(0, 4096)));
963
1446
  child.once('error', (error) => {
@@ -986,4 +1469,3 @@ function summarizeText(value) {
986
1469
  bytes: Buffer.byteLength(value)
987
1470
  });
988
1471
  }
989
- //# sourceMappingURL=workspace.js.map