@oh-my-pi/pi-coding-agent 16.2.9 → 16.2.11
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.
- package/CHANGELOG.md +29 -0
- package/dist/cli.js +3178 -3177
- package/dist/types/extensibility/skills.d.ts +29 -0
- package/dist/types/modes/components/todo-reminder.d.ts +3 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +0 -1
- package/dist/types/modes/skill-command.d.ts +1 -1
- package/dist/types/modes/types.d.ts +0 -1
- package/dist/types/stt/asr-client.d.ts +7 -3
- package/package.json +14 -13
- package/scripts/bundle-dist.ts +23 -4
- package/scripts/generate-docs-index.ts +116 -24
- package/src/async/job-manager.ts +27 -3
- package/src/cli/grep-cli.ts +1 -1
- package/src/config/model-discovery.ts +118 -76
- package/src/extensibility/skills.ts +77 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/lsp/config.ts +17 -3
- package/src/modes/acp/acp-agent.ts +6 -9
- package/src/modes/components/todo-reminder.ts +5 -1
- package/src/modes/controllers/event-controller.ts +40 -24
- package/src/modes/controllers/selector-controller.ts +57 -35
- package/src/modes/controllers/tool-args-reveal.ts +12 -0
- package/src/modes/interactive-mode.ts +0 -6
- package/src/modes/rpc/rpc-mode.ts +5 -8
- package/src/modes/skill-command.ts +8 -20
- package/src/modes/types.ts +0 -1
- package/src/stt/asr-client.ts +87 -27
- package/src/stt/downloader.ts +8 -2
- package/src/task/executor.ts +1 -3
- package/src/task/index.ts +14 -5
- package/src/tools/ast-grep.ts +34 -12
- package/src/tools/grep.ts +11 -8
- package/src/utils/git.ts +22 -1
package/src/stt/asr-client.ts
CHANGED
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
createWorkerHandle,
|
|
5
5
|
createWorkerSubprocess,
|
|
6
6
|
logWorkerMessage,
|
|
7
|
+
type RefCountedWorkerHandle,
|
|
7
8
|
resolveWorkerSpawnCmd,
|
|
8
9
|
SMOKE_TEST_TIMEOUT_MS,
|
|
9
10
|
type SpawnedSubprocess,
|
|
10
11
|
smokeTestWorker,
|
|
11
12
|
spawnWorkerOrUnavailable,
|
|
12
|
-
type WorkerHandle,
|
|
13
13
|
} from "../subprocess/worker-client";
|
|
14
14
|
import { tinyWorkerEnv } from "../tiny/title-client";
|
|
15
15
|
import { safeSend } from "../utils/ipc";
|
|
@@ -18,7 +18,7 @@ import type { SttModelKey } from "./models";
|
|
|
18
18
|
|
|
19
19
|
type PendingRequest =
|
|
20
20
|
| { kind: "transcribe"; modelKey: SttModelKey; resolve: (text: string) => void; reject: (error: Error) => void }
|
|
21
|
-
| { kind: "download"; modelKey: SttModelKey; resolve: (
|
|
21
|
+
| { kind: "download"; modelKey: SttModelKey; resolve: (result: SttDownloadResult) => void };
|
|
22
22
|
|
|
23
23
|
export interface SttTranscribeOptions {
|
|
24
24
|
language?: string;
|
|
@@ -30,6 +30,11 @@ export interface SttDownloadOptions {
|
|
|
30
30
|
onProgress?: (event: SttProgressEvent) => void;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export interface SttDownloadResult {
|
|
34
|
+
ok: boolean;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
/** Live streaming session handle returned by {@link SttClient.startStream}. */
|
|
34
39
|
export interface SttStreamHandle {
|
|
35
40
|
/** Feed 16 kHz mono float samples as the recorder produces them. */
|
|
@@ -79,30 +84,55 @@ export function createSttSubprocess(): SpawnedSubprocess<SttWorkerOutbound> {
|
|
|
79
84
|
|
|
80
85
|
function wrapSubprocess(
|
|
81
86
|
spawned: SpawnedSubprocess<SttWorkerOutbound>,
|
|
82
|
-
):
|
|
87
|
+
): RefCountedWorkerHandle<SttWorkerInbound, SttWorkerOutbound> {
|
|
83
88
|
const { proc } = spawned;
|
|
84
|
-
return
|
|
89
|
+
return {
|
|
90
|
+
...createWorkerHandle<SttWorkerInbound, SttWorkerOutbound>(spawned, message => safeSend(proc, message, "stt")),
|
|
91
|
+
ref() {
|
|
92
|
+
try {
|
|
93
|
+
proc.ref();
|
|
94
|
+
} catch {
|
|
95
|
+
// Already gone.
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
unref() {
|
|
99
|
+
try {
|
|
100
|
+
proc.unref();
|
|
101
|
+
} catch {
|
|
102
|
+
// Already gone.
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function spawnInlineUnavailableWorker(error: unknown): RefCountedWorkerHandle<SttWorkerInbound, SttWorkerOutbound> {
|
|
109
|
+
return {
|
|
110
|
+
...createUnavailableWorker<SttWorkerInbound, SttWorkerOutbound>(error),
|
|
111
|
+
ref() {},
|
|
112
|
+
unref() {},
|
|
113
|
+
};
|
|
85
114
|
}
|
|
86
115
|
|
|
87
|
-
function spawnSttWorker():
|
|
116
|
+
function spawnSttWorker(): RefCountedWorkerHandle<SttWorkerInbound, SttWorkerOutbound> {
|
|
88
117
|
return spawnWorkerOrUnavailable(
|
|
89
118
|
() => wrapSubprocess(createSttSubprocess()),
|
|
90
|
-
|
|
119
|
+
spawnInlineUnavailableWorker,
|
|
91
120
|
"stt worker spawn failed; speech-to-text disabled",
|
|
92
121
|
);
|
|
93
122
|
}
|
|
94
123
|
|
|
95
124
|
export class SttClient {
|
|
96
|
-
#worker:
|
|
125
|
+
#worker: RefCountedWorkerHandle<SttWorkerInbound, SttWorkerOutbound> | null = null;
|
|
97
126
|
#unsubscribeMessage: (() => void) | null = null;
|
|
98
127
|
#unsubscribeError: (() => void) | null = null;
|
|
99
128
|
#pending = new Map<string, PendingRequest>();
|
|
100
129
|
#streams = new Map<string, StreamState>();
|
|
101
130
|
#progressListeners = new Set<(event: SttProgressEvent) => void>();
|
|
102
131
|
#nextRequestId = 0;
|
|
103
|
-
#
|
|
132
|
+
#refed = false;
|
|
133
|
+
#spawnWorker: () => RefCountedWorkerHandle<SttWorkerInbound, SttWorkerOutbound>;
|
|
104
134
|
|
|
105
|
-
constructor(spawnWorker: () =>
|
|
135
|
+
constructor(spawnWorker: () => RefCountedWorkerHandle<SttWorkerInbound, SttWorkerOutbound> = spawnSttWorker) {
|
|
106
136
|
this.#spawnWorker = spawnWorker;
|
|
107
137
|
}
|
|
108
138
|
|
|
@@ -121,11 +151,11 @@ export class SttClient {
|
|
|
121
151
|
const worker = this.#ensureWorker();
|
|
122
152
|
const id = String(++this.#nextRequestId);
|
|
123
153
|
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
|
124
|
-
this.#
|
|
154
|
+
this.#addPending(id, { kind: "transcribe", modelKey, resolve, reject });
|
|
125
155
|
const abort = (): void => {
|
|
126
156
|
const pending = this.#pending.get(id);
|
|
127
157
|
if (pending?.kind !== "transcribe") return;
|
|
128
|
-
this.#
|
|
158
|
+
this.#deletePending(id);
|
|
129
159
|
pending.reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
130
160
|
};
|
|
131
161
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
@@ -134,7 +164,7 @@ export class SttClient {
|
|
|
134
164
|
return await promise;
|
|
135
165
|
} finally {
|
|
136
166
|
options.signal?.removeEventListener("abort", abort);
|
|
137
|
-
this.#
|
|
167
|
+
this.#deletePending(id);
|
|
138
168
|
}
|
|
139
169
|
}
|
|
140
170
|
|
|
@@ -163,6 +193,7 @@ export class SttClient {
|
|
|
163
193
|
settled = true;
|
|
164
194
|
this.#streams.delete(id);
|
|
165
195
|
signal?.removeEventListener("abort", onAbort);
|
|
196
|
+
this.#syncWorkerRef();
|
|
166
197
|
apply();
|
|
167
198
|
};
|
|
168
199
|
this.#streams.set(id, {
|
|
@@ -173,6 +204,7 @@ export class SttClient {
|
|
|
173
204
|
reject,
|
|
174
205
|
finish,
|
|
175
206
|
});
|
|
207
|
+
this.#syncWorkerRef();
|
|
176
208
|
worker.send({ type: "stream_start", id, modelKey, language: options.language });
|
|
177
209
|
const handle: SttStreamHandle = {
|
|
178
210
|
pushAudio: audio => {
|
|
@@ -193,19 +225,19 @@ export class SttClient {
|
|
|
193
225
|
return handle;
|
|
194
226
|
}
|
|
195
227
|
|
|
196
|
-
async downloadModel(modelKey: SttModelKey, options: SttDownloadOptions = {}): Promise<
|
|
197
|
-
if (options.signal?.aborted) return false;
|
|
228
|
+
async downloadModel(modelKey: SttModelKey, options: SttDownloadOptions = {}): Promise<SttDownloadResult> {
|
|
229
|
+
if (options.signal?.aborted) return { ok: false };
|
|
198
230
|
const unsubscribe = options.onProgress ? this.onProgress(options.onProgress) : undefined;
|
|
199
231
|
try {
|
|
200
232
|
const worker = this.#ensureWorker();
|
|
201
233
|
const id = String(++this.#nextRequestId);
|
|
202
|
-
const { promise, resolve } = Promise.withResolvers<
|
|
203
|
-
this.#
|
|
234
|
+
const { promise, resolve } = Promise.withResolvers<SttDownloadResult>();
|
|
235
|
+
this.#addPending(id, { kind: "download", modelKey, resolve });
|
|
204
236
|
const abort = (): void => {
|
|
205
237
|
const pending = this.#pending.get(id);
|
|
206
238
|
if (pending?.kind !== "download") return;
|
|
207
|
-
this.#
|
|
208
|
-
pending.resolve(false);
|
|
239
|
+
this.#deletePending(id);
|
|
240
|
+
pending.resolve({ ok: false });
|
|
209
241
|
};
|
|
210
242
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
211
243
|
try {
|
|
@@ -213,14 +245,15 @@ export class SttClient {
|
|
|
213
245
|
return await promise;
|
|
214
246
|
} finally {
|
|
215
247
|
options.signal?.removeEventListener("abort", abort);
|
|
216
|
-
this.#
|
|
248
|
+
this.#deletePending(id);
|
|
217
249
|
}
|
|
218
250
|
} catch (error) {
|
|
251
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
219
252
|
logger.debug("stt: local model download failed", {
|
|
220
253
|
modelKey,
|
|
221
|
-
error:
|
|
254
|
+
error: message,
|
|
222
255
|
});
|
|
223
|
-
return false;
|
|
256
|
+
return { ok: false, error: message };
|
|
224
257
|
} finally {
|
|
225
258
|
unsubscribe?.();
|
|
226
259
|
}
|
|
@@ -236,9 +269,10 @@ export class SttClient {
|
|
|
236
269
|
for (const pending of this.#pending.values()) {
|
|
237
270
|
this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
|
|
238
271
|
if (pending.kind === "transcribe") pending.reject(new Error("stt worker terminated"));
|
|
239
|
-
else pending.resolve(false);
|
|
272
|
+
else pending.resolve({ ok: false });
|
|
240
273
|
}
|
|
241
274
|
this.#pending.clear();
|
|
275
|
+
this.#refed = false;
|
|
242
276
|
this.#failStreams(new Error("stt worker terminated"));
|
|
243
277
|
try {
|
|
244
278
|
await worker?.terminate();
|
|
@@ -247,7 +281,7 @@ export class SttClient {
|
|
|
247
281
|
}
|
|
248
282
|
}
|
|
249
283
|
|
|
250
|
-
#ensureWorker():
|
|
284
|
+
#ensureWorker(): RefCountedWorkerHandle<SttWorkerInbound, SttWorkerOutbound> {
|
|
251
285
|
if (this.#worker) return this.#worker;
|
|
252
286
|
const worker = this.#spawnWorker();
|
|
253
287
|
this.#worker = worker;
|
|
@@ -256,6 +290,32 @@ export class SttClient {
|
|
|
256
290
|
return worker;
|
|
257
291
|
}
|
|
258
292
|
|
|
293
|
+
/** Register a pending request and keep the worker referenced while work is in flight. */
|
|
294
|
+
#addPending(id: string, request: PendingRequest): void {
|
|
295
|
+
this.#pending.set(id, request);
|
|
296
|
+
this.#syncWorkerRef();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Drop a pending request and unref the worker once no request or stream is active. */
|
|
300
|
+
#deletePending(id: string): void {
|
|
301
|
+
if (this.#pending.delete(id)) this.#syncWorkerRef();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* STT workers start unreferenced so an idle warm model never blocks exit.
|
|
306
|
+
* Setup/download commands must keep the worker alive while awaiting IPC, or
|
|
307
|
+
* Bun can drain the event loop immediately after `Preparing Speech-to-Text`.
|
|
308
|
+
*/
|
|
309
|
+
#syncWorkerRef(): void {
|
|
310
|
+
const worker = this.#worker;
|
|
311
|
+
if (!worker) return;
|
|
312
|
+
const shouldRef = this.#pending.size > 0 || this.#streams.size > 0;
|
|
313
|
+
if (shouldRef === this.#refed) return;
|
|
314
|
+
this.#refed = shouldRef;
|
|
315
|
+
if (shouldRef) worker.ref();
|
|
316
|
+
else worker.unref();
|
|
317
|
+
}
|
|
318
|
+
|
|
259
319
|
#handleMessage(message: SttWorkerOutbound): void {
|
|
260
320
|
if (message.type === "log") {
|
|
261
321
|
logWorkerMessage(message);
|
|
@@ -287,19 +347,19 @@ export class SttClient {
|
|
|
287
347
|
}
|
|
288
348
|
return;
|
|
289
349
|
}
|
|
290
|
-
this.#
|
|
350
|
+
this.#deletePending(message.id);
|
|
291
351
|
if (message.type === "transcription") {
|
|
292
352
|
if (pending.kind === "transcribe") pending.resolve(message.text);
|
|
293
353
|
return;
|
|
294
354
|
}
|
|
295
355
|
if (message.type === "downloaded") {
|
|
296
|
-
if (pending.kind === "download") pending.resolve(true);
|
|
356
|
+
if (pending.kind === "download") pending.resolve({ ok: true });
|
|
297
357
|
return;
|
|
298
358
|
}
|
|
299
359
|
// message.type === "error"
|
|
300
360
|
this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
|
|
301
361
|
if (pending.kind === "transcribe") pending.reject(new Error(message.error));
|
|
302
|
-
else pending.resolve(false);
|
|
362
|
+
else pending.resolve({ ok: false, error: message.error });
|
|
303
363
|
}
|
|
304
364
|
|
|
305
365
|
#emitProgress(event: SttProgressEvent): void {
|
|
@@ -318,7 +378,7 @@ export class SttClient {
|
|
|
318
378
|
for (const pending of this.#pending.values()) {
|
|
319
379
|
this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
|
|
320
380
|
if (pending.kind === "transcribe") pending.reject(error);
|
|
321
|
-
else pending.resolve(false);
|
|
381
|
+
else pending.resolve({ ok: false, error: error.message });
|
|
322
382
|
}
|
|
323
383
|
this.#pending.clear();
|
|
324
384
|
this.#failStreams(error);
|
package/src/stt/downloader.ts
CHANGED
|
@@ -90,7 +90,7 @@ export async function downloadSttModel(
|
|
|
90
90
|
): Promise<void> {
|
|
91
91
|
const spec = resolveSttModelSpec(key);
|
|
92
92
|
const files = new Map<string, { loaded: number; total: number }>();
|
|
93
|
-
const
|
|
93
|
+
const result = await sttClient.downloadModel(spec.key, {
|
|
94
94
|
signal: options?.signal,
|
|
95
95
|
onProgress: event => {
|
|
96
96
|
if ((event.status === "progress" || event.status === "progress_total") && event.file) {
|
|
@@ -117,7 +117,13 @@ export async function downloadSttModel(
|
|
|
117
117
|
});
|
|
118
118
|
},
|
|
119
119
|
});
|
|
120
|
-
if (!ok)
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
const detail = result.error ? `: ${result.error}` : ". Check your network connection.";
|
|
122
|
+
throw new Error(`Failed to download speech model (${spec.repo})${detail}`);
|
|
123
|
+
}
|
|
124
|
+
if (!(await isSttModelCached(spec.key))) {
|
|
125
|
+
throw new Error(`Speech model download finished without required files (${spec.repo}).`);
|
|
126
|
+
}
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
// ── Public API ─────────────────────────────────────────────────────
|
package/src/task/executor.ts
CHANGED
|
@@ -2053,9 +2053,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2053
2053
|
? formatModelSelectorValue(formatModelStringWithRouting(model), resolvedThinkingLevel)
|
|
2054
2054
|
: formatModelStringWithRouting(model);
|
|
2055
2055
|
}
|
|
2056
|
-
const effectiveThinkingLevel =
|
|
2057
|
-
? resolvedThinkingLevel
|
|
2058
|
-
: (thinkingLevel ?? resolvedThinkingLevel);
|
|
2056
|
+
const effectiveThinkingLevel = thinkingLevel ?? resolvedThinkingLevel;
|
|
2059
2057
|
resolvedAt = performance.now();
|
|
2060
2058
|
|
|
2061
2059
|
const effectiveCwd = worktree ?? cwd;
|
package/src/task/index.ts
CHANGED
|
@@ -803,10 +803,19 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
803
803
|
async ({ jobId: ownJobId, signal: runSignal, reportProgress, markRunning }) => {
|
|
804
804
|
const startedAt = Date.now();
|
|
805
805
|
const semaphore = this.#getSpawnSemaphore();
|
|
806
|
-
|
|
806
|
+
let semaphoreHeld = false;
|
|
807
|
+
try {
|
|
808
|
+
await semaphore.acquire(runSignal);
|
|
809
|
+
semaphoreHeld = true;
|
|
810
|
+
} catch {
|
|
811
|
+
// Fall through so an acquire-time abort goes through the same
|
|
812
|
+
// path as the post-acquire race below: progress + onSettled
|
|
813
|
+
// have to fire even when the spawn never reached the executor,
|
|
814
|
+
// otherwise the batch aggregate state stays "running" forever.
|
|
815
|
+
}
|
|
807
816
|
const acquiredAt = Date.now();
|
|
808
|
-
if (runSignal.aborted) {
|
|
809
|
-
semaphore.release();
|
|
817
|
+
if (!semaphoreHeld || runSignal.aborted) {
|
|
818
|
+
if (semaphoreHeld) semaphore.release();
|
|
810
819
|
progress.status = "aborted";
|
|
811
820
|
onSettled?.(true);
|
|
812
821
|
throw new Error("Aborted before execution");
|
|
@@ -909,7 +918,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
909
918
|
const semaphore = this.#getSpawnSemaphore();
|
|
910
919
|
if (spawnItems.length === 1) {
|
|
911
920
|
const invokedAt = Date.now();
|
|
912
|
-
await semaphore.acquire();
|
|
921
|
+
await semaphore.acquire(signal);
|
|
913
922
|
const acquiredAt = Date.now();
|
|
914
923
|
try {
|
|
915
924
|
return await this.#executeSync(
|
|
@@ -948,7 +957,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
948
957
|
spawnItems.length,
|
|
949
958
|
async (item, index, workerSignal) => {
|
|
950
959
|
const invokedAt = Date.now();
|
|
951
|
-
await semaphore.acquire();
|
|
960
|
+
await semaphore.acquire(workerSignal);
|
|
952
961
|
const acquiredAt = Date.now();
|
|
953
962
|
try {
|
|
954
963
|
const itemOnUpdate: AgentToolUpdateCallback<TaskToolDetails> | undefined = onUpdate
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -44,6 +44,33 @@ const astGrepSchema = type({
|
|
|
44
44
|
"skip?": type("number").describe("matches to skip"),
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
function compareAstFindMatch(left: AstFindMatch, right: AstFindMatch): number {
|
|
48
|
+
const pathCmp = left.path.localeCompare(right.path);
|
|
49
|
+
if (pathCmp !== 0) return pathCmp;
|
|
50
|
+
if (left.startLine !== right.startLine) return left.startLine - right.startLine;
|
|
51
|
+
if (left.startColumn !== right.startColumn) return left.startColumn - right.startColumn;
|
|
52
|
+
if (left.endLine !== right.endLine) return left.endLine - right.endLine;
|
|
53
|
+
if (left.endColumn !== right.endColumn) return left.endColumn - right.endColumn;
|
|
54
|
+
if (left.byteStart !== right.byteStart) return left.byteStart - right.byteStart;
|
|
55
|
+
return left.byteEnd - right.byteEnd;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function retainAstFindMatch(matches: AstFindMatch[], capacity: number, candidate: AstFindMatch): void {
|
|
59
|
+
if (matches.length < capacity) {
|
|
60
|
+
matches.push(candidate);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
let worstIndex = 0;
|
|
64
|
+
for (let index = 1; index < matches.length; index++) {
|
|
65
|
+
if (compareAstFindMatch(matches[index]!, matches[worstIndex]!) > 0) {
|
|
66
|
+
worstIndex = index;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (compareAstFindMatch(candidate, matches[worstIndex]!) < 0) {
|
|
70
|
+
matches[worstIndex] = candidate;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
47
74
|
async function runMultiTargetAstGrep(
|
|
48
75
|
targets: Array<{ basePath: string; glob?: string }>,
|
|
49
76
|
options: { patterns: string[]; commonBasePath: string; skip: number; limit: number; signal?: AbortSignal },
|
|
@@ -55,9 +82,11 @@ async function runMultiTargetAstGrep(
|
|
|
55
82
|
limitReached: boolean;
|
|
56
83
|
parseErrors?: string[];
|
|
57
84
|
}> {
|
|
58
|
-
const
|
|
85
|
+
const retainedMatches: AstFindMatch[] = [];
|
|
86
|
+
const retainedCapacity = options.skip + options.limit + 1;
|
|
59
87
|
const parseErrors: string[] = [];
|
|
60
88
|
let totalMatches = 0;
|
|
89
|
+
let filesWithMatches = 0;
|
|
61
90
|
let filesSearched = 0;
|
|
62
91
|
let limitReached = false;
|
|
63
92
|
for (const target of targets) {
|
|
@@ -71,26 +100,19 @@ async function runMultiTargetAstGrep(
|
|
|
71
100
|
signal: options.signal,
|
|
72
101
|
});
|
|
73
102
|
totalMatches += targetResult.totalMatches;
|
|
103
|
+
filesWithMatches += targetResult.filesWithMatches;
|
|
74
104
|
filesSearched += targetResult.filesSearched;
|
|
75
105
|
limitReached = limitReached || targetResult.limitReached;
|
|
76
106
|
if (targetResult.parseErrors) parseErrors.push(...targetResult.parseErrors);
|
|
77
107
|
for (const match of targetResult.matches) {
|
|
78
108
|
const absolute = path.resolve(target.basePath, match.path);
|
|
79
109
|
const rebased = path.relative(options.commonBasePath, absolute).replace(/\\/g, "/");
|
|
80
|
-
|
|
110
|
+
retainAstFindMatch(retainedMatches, retainedCapacity, { ...match, path: rebased });
|
|
81
111
|
}
|
|
82
112
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (pathCmp !== 0) return pathCmp;
|
|
86
|
-
if (left.startLine !== right.startLine) return left.startLine - right.startLine;
|
|
87
|
-
if (left.startColumn !== right.startColumn) return left.startColumn - right.startColumn;
|
|
88
|
-
if (left.byteStart !== right.byteStart) return left.byteStart - right.byteStart;
|
|
89
|
-
return left.byteEnd - right.byteEnd;
|
|
90
|
-
});
|
|
91
|
-
const visible = aggregatedMatches.slice(options.skip);
|
|
113
|
+
retainedMatches.sort(compareAstFindMatch);
|
|
114
|
+
const visible = retainedMatches.slice(options.skip);
|
|
92
115
|
const paged = visible.slice(0, options.limit);
|
|
93
|
-
const filesWithMatches = new Set(aggregatedMatches.map(match => match.path)).size;
|
|
94
116
|
return {
|
|
95
117
|
matches: paged,
|
|
96
118
|
totalMatches,
|
package/src/tools/grep.ts
CHANGED
|
@@ -122,8 +122,10 @@ export const SINGLE_FILE_MATCHES = 200;
|
|
|
122
122
|
* pagination headroom so the caller can see total file count. */
|
|
123
123
|
const INTERNAL_TOTAL_CAP = 2000;
|
|
124
124
|
/** Mirrors `MAX_FILE_BYTES` in `crates/pi-natives/src/grep.rs`. Native grep
|
|
125
|
-
*
|
|
126
|
-
*
|
|
125
|
+
* searches only the first `MAX_FILE_BYTES` of a larger file (a leading mmap
|
|
126
|
+
* window) and drops the rest; matches beyond the window are not returned. We
|
|
127
|
+
* surface a partial-coverage note when the caller explicitly targeted such a
|
|
128
|
+
* file so they know matches past the window are not shown. */
|
|
127
129
|
const NATIVE_GREP_MAX_FILE_BYTES = 4 * 1024 * 1024;
|
|
128
130
|
/** Wall-clock budget for a single native grep invocation. Without it, an
|
|
129
131
|
* aborted or runaway search (huge tree, network mount) keeps burning CPU on
|
|
@@ -1260,8 +1262,9 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1260
1262
|
const { record: recordFile, list: fileList } = createFileRecorder();
|
|
1261
1263
|
const fileMatchCounts = new Map<string, number>();
|
|
1262
1264
|
// Detect explicit file targets that exceed the native grep size cap.
|
|
1263
|
-
// Native
|
|
1264
|
-
//
|
|
1265
|
+
// Native searches only their first NATIVE_GREP_MAX_FILE_BYTES; without
|
|
1266
|
+
// this note the caller might miss that matches beyond the window
|
|
1267
|
+
// (or "no matches") reflect partial coverage, not the whole file.
|
|
1265
1268
|
const oversizedNote = await (async (): Promise<string | undefined> => {
|
|
1266
1269
|
const explicitFileTargets: string[] = [];
|
|
1267
1270
|
if (exactFilePaths) {
|
|
@@ -1285,13 +1288,13 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1285
1288
|
);
|
|
1286
1289
|
if (oversized.length === 0) return undefined;
|
|
1287
1290
|
const limitMb = Math.floor(NATIVE_GREP_MAX_FILE_BYTES / (1024 * 1024));
|
|
1288
|
-
return `
|
|
1291
|
+
return `Searched only the first ${limitMb}MB of large files (matches past the ${limitMb}MB window are not shown; use \`read\` for the rest): ${oversized.join(", ")}`;
|
|
1289
1292
|
})();
|
|
1290
|
-
// Directory/multi-target scopes: native
|
|
1291
|
-
//
|
|
1293
|
+
// Directory/multi-target scopes: native counts files it could not map
|
|
1294
|
+
// even a prefix of (rare mmap failures), but cannot name them.
|
|
1292
1295
|
const oversizedScanNote =
|
|
1293
1296
|
!oversizedNote && skippedOversizedCount > 0
|
|
1294
|
-
? `Skipped ${skippedOversizedCount}
|
|
1297
|
+
? `Skipped ${skippedOversizedCount} unreadable large file(s); target them directly with \`read\``
|
|
1295
1298
|
: undefined;
|
|
1296
1299
|
const archiveNote =
|
|
1297
1300
|
archiveUnreadable.length > 0
|
package/src/utils/git.ts
CHANGED
|
@@ -175,6 +175,14 @@ const SHORT_LIVED_GIT_CONFIG: readonly (readonly [key: string, value: string])[]
|
|
|
175
175
|
["core.untrackedCache", "false"],
|
|
176
176
|
];
|
|
177
177
|
const REMOTE_ALREADY_EXISTS = /remote .* already exists/i;
|
|
178
|
+
const AMBIENT_GIT_ENV = {
|
|
179
|
+
GIT_DIR: undefined,
|
|
180
|
+
GIT_COMMON_DIR: undefined,
|
|
181
|
+
GIT_WORK_TREE: undefined,
|
|
182
|
+
GIT_INDEX_FILE: undefined,
|
|
183
|
+
GIT_OBJECT_DIRECTORY: undefined,
|
|
184
|
+
GIT_ALTERNATE_OBJECT_DIRECTORIES: undefined,
|
|
185
|
+
} satisfies Record<string, undefined>;
|
|
178
186
|
|
|
179
187
|
interface CommandOptions {
|
|
180
188
|
readonly env?: Record<string, string | undefined>;
|
|
@@ -190,6 +198,15 @@ function normalizeStdin(input: CommandOptions["stdin"]): "ignore" | Uint8Array {
|
|
|
190
198
|
return new Uint8Array(input);
|
|
191
199
|
}
|
|
192
200
|
|
|
201
|
+
function buildGitEnv(overrides?: Record<string, string | undefined>): Record<string, string | undefined> {
|
|
202
|
+
return {
|
|
203
|
+
...process.env,
|
|
204
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
205
|
+
...AMBIENT_GIT_ENV,
|
|
206
|
+
...overrides,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
193
210
|
function ensureAvailable(): void {
|
|
194
211
|
if (!$which("git")) {
|
|
195
212
|
throw new Error("git is not installed.");
|
|
@@ -211,7 +228,7 @@ async function git(cwd: string, args: readonly string[], options: CommandOptions
|
|
|
211
228
|
const commandArgs = withShortLivedGitConfig(options.readOnly ? withNoOptionalLocks(args) : [...args]);
|
|
212
229
|
const child = Bun.spawn(["git", ...commandArgs], {
|
|
213
230
|
cwd,
|
|
214
|
-
env: options.env
|
|
231
|
+
env: buildGitEnv(options.env),
|
|
215
232
|
signal: options.signal,
|
|
216
233
|
stdin: normalizeStdin(options.stdin),
|
|
217
234
|
stdout: "pipe",
|
|
@@ -707,6 +724,7 @@ function resolveHeadStateReftableSync(repository: GitRepository): GitHeadState |
|
|
|
707
724
|
const symArgs = withShortLivedGitConfig(withNoOptionalLocks(["symbolic-ref", "HEAD"]));
|
|
708
725
|
const symResult = Bun.spawnSync(["git", ...symArgs], {
|
|
709
726
|
cwd: repository.repoRoot,
|
|
727
|
+
env: buildGitEnv(),
|
|
710
728
|
stdout: "pipe",
|
|
711
729
|
stderr: "pipe",
|
|
712
730
|
windowsHide: true,
|
|
@@ -715,6 +733,7 @@ function resolveHeadStateReftableSync(repository: GitRepository): GitHeadState |
|
|
|
715
733
|
const revArgs = withShortLivedGitConfig(withNoOptionalLocks(["rev-parse", "--verify", "HEAD"]));
|
|
716
734
|
const revResult = Bun.spawnSync(["git", ...revArgs], {
|
|
717
735
|
cwd: repository.repoRoot,
|
|
736
|
+
env: buildGitEnv(),
|
|
718
737
|
stdout: "pipe",
|
|
719
738
|
stderr: "pipe",
|
|
720
739
|
windowsHide: true,
|
|
@@ -748,6 +767,7 @@ function readRefSync(repository: GitRepository, targetRef: string): string | nul
|
|
|
748
767
|
const symArgs = withShortLivedGitConfig(withNoOptionalLocks(["symbolic-ref", targetRef]));
|
|
749
768
|
const symResult = Bun.spawnSync(["git", ...symArgs], {
|
|
750
769
|
cwd: repository.repoRoot,
|
|
770
|
+
env: buildGitEnv(),
|
|
751
771
|
stdout: "pipe",
|
|
752
772
|
stderr: "pipe",
|
|
753
773
|
windowsHide: true,
|
|
@@ -759,6 +779,7 @@ function readRefSync(repository: GitRepository, targetRef: string): string | nul
|
|
|
759
779
|
const revArgs = withShortLivedGitConfig(withNoOptionalLocks(["rev-parse", "--verify", targetRef]));
|
|
760
780
|
const revResult = Bun.spawnSync(["git", ...revArgs], {
|
|
761
781
|
cwd: repository.repoRoot,
|
|
782
|
+
env: buildGitEnv(),
|
|
762
783
|
stdout: "pipe",
|
|
763
784
|
stderr: "pipe",
|
|
764
785
|
windowsHide: true,
|