mixdog 0.9.93 → 0.9.94
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/package.json +1 -1
- package/src/rules/agent/30-explorer.md +22 -16
- package/src/rules/shared/01-tool.md +23 -18
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +22 -6
- package/src/runtime/agent/orchestrator/context/collect.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +75 -15
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +42 -2
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +100 -86
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +98 -3
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/builtin/grep-formatting.mjs +22 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +491 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +91 -13
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +90 -27
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +19 -9
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +32 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +16 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +543 -19
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +48 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +71 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +51 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +6 -17
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -4
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +1 -1
- package/src/runtime/memory/lib/tool-call-handler.mjs +16 -1
- package/src/runtime/shared/background-tasks.mjs +10 -3
- package/src/runtime/shared/child-spawn-gate.mjs +50 -26
- package/src/runtime/shared/task-notification-envelope.mjs +11 -2
- package/src/runtime/shared/tool-card-model.mjs +6 -2
- package/src/runtime/shared/tool-surface.mjs +7 -2
- package/src/session-runtime/provider-usage.mjs +26 -2
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/tui/app/use-transcript-window.mjs +7 -1
- package/src/tui/components/Spinner.jsx +18 -9
- package/src/tui/dist/index.mjs +111 -25
- package/src/tui/engine/live-share.mjs +23 -3
- package/src/tui/engine/session-api.mjs +7 -0
- package/src/tui/engine/turn.mjs +77 -4
- package/src/tui/engine.mjs +14 -6
- package/src/tui/index.jsx +7 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
normalizeOutputPath,
|
|
7
|
+
} from '../path-utils.mjs';
|
|
8
|
+
import {
|
|
9
|
+
splitGrepLineNumberOnlyPrefix,
|
|
10
|
+
splitGrepLinePrefix,
|
|
11
|
+
} from '../grep-formatting.mjs';
|
|
12
|
+
import {
|
|
13
|
+
relativePathPrefix,
|
|
14
|
+
} from '../search-path-diagnostics.mjs';
|
|
15
|
+
import {
|
|
16
|
+
relativeGrepLine,
|
|
17
|
+
} from './search-input-helpers.mjs';
|
|
18
|
+
|
|
19
|
+
export const GREP_CONTEXT_CHAR_BUDGET_DEFAULT = 10_000;
|
|
20
|
+
const GREP_CONTEXT_READ_CONCURRENCY = 4;
|
|
21
|
+
const GREP_FOCUSED_CONTEXT_RADIUS = 12;
|
|
22
|
+
const GREP_FOCUSED_RAW_BLOCKS = 3;
|
|
23
|
+
// Anchors must stay usable as evidence without a follow-up read: keep the
|
|
24
|
+
// complete match line and only cut pathological long lines (minified files).
|
|
25
|
+
// Mid-line ellipsis below ~100 chars measurably pushed models into re-read
|
|
26
|
+
// verification loops (tool-budget bench, 20260805).
|
|
27
|
+
const GREP_COMPACT_ANCHOR_CONTENT_MAX = 400;
|
|
28
|
+
|
|
29
|
+
function parseAnchor(line, {
|
|
30
|
+
workDir,
|
|
31
|
+
rgSpawnCwd,
|
|
32
|
+
grepResolvedPath,
|
|
33
|
+
searchPath,
|
|
34
|
+
outputMode,
|
|
35
|
+
filenameOmitted,
|
|
36
|
+
}) {
|
|
37
|
+
if (filenameOmitted) {
|
|
38
|
+
const split = splitGrepLineNumberOnlyPrefix(line);
|
|
39
|
+
if (!split || split.delimiter !== ':') return null;
|
|
40
|
+
return {
|
|
41
|
+
path: relativePathPrefix(normalizeOutputPath(searchPath), workDir),
|
|
42
|
+
absolutePath: grepResolvedPath,
|
|
43
|
+
lineNo: split.lineNo,
|
|
44
|
+
content: String(line).slice(split.markerEnd),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const raw = splitGrepLinePrefix(line);
|
|
48
|
+
if (!raw || raw.delimiter !== ':') return null;
|
|
49
|
+
const normalized = relativeGrepLine(line, workDir, false, outputMode, false);
|
|
50
|
+
const display = splitGrepLinePrefix(normalized);
|
|
51
|
+
if (!display || display.delimiter !== ':') return null;
|
|
52
|
+
return {
|
|
53
|
+
path: display.path,
|
|
54
|
+
absolutePath: isAbsolute(raw.path) ? raw.path : resolve(rgSpawnCwd, raw.path),
|
|
55
|
+
lineNo: raw.lineNo,
|
|
56
|
+
content: String(line).slice(raw.markerEnd),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseAnchors(allLines, options) {
|
|
61
|
+
const anchors = [];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
for (const line of allLines) {
|
|
64
|
+
const anchor = parseAnchor(line, options);
|
|
65
|
+
if (!anchor) continue;
|
|
66
|
+
const key = `${anchor.absolutePath}\0${anchor.lineNo}`;
|
|
67
|
+
if (seen.has(key)) continue;
|
|
68
|
+
seen.add(key);
|
|
69
|
+
anchors.push({ ...anchor, order: anchors.length });
|
|
70
|
+
}
|
|
71
|
+
return anchors;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function splitTopLevelAlternatives(pattern) {
|
|
75
|
+
const source = String(pattern || '');
|
|
76
|
+
const parts = [];
|
|
77
|
+
let start = 0;
|
|
78
|
+
let depth = 0;
|
|
79
|
+
let inClass = false;
|
|
80
|
+
let escaped = false;
|
|
81
|
+
for (let index = 0; index < source.length; index++) {
|
|
82
|
+
const char = source[index];
|
|
83
|
+
if (escaped) {
|
|
84
|
+
escaped = false;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (char === '\\') {
|
|
88
|
+
escaped = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (inClass) {
|
|
92
|
+
if (char === ']') inClass = false;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (char === '[') {
|
|
96
|
+
inClass = true;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (char === '(') {
|
|
100
|
+
depth++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (char === ')' && depth > 0) {
|
|
104
|
+
depth--;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (char === '|' && depth === 0) {
|
|
108
|
+
parts.push(source.slice(start, index));
|
|
109
|
+
start = index + 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (parts.length === 0) return [source];
|
|
113
|
+
parts.push(source.slice(start));
|
|
114
|
+
return parts;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function rankAnchors(anchors, patterns, caseInsensitive) {
|
|
118
|
+
const branches = [];
|
|
119
|
+
for (const pattern of Array.isArray(patterns) ? patterns : [patterns]) {
|
|
120
|
+
for (const branch of splitTopLevelAlternatives(pattern)) {
|
|
121
|
+
if (!branch) continue;
|
|
122
|
+
try {
|
|
123
|
+
branches.push(new RegExp(branch, caseInsensitive ? 'i' : ''));
|
|
124
|
+
} catch {
|
|
125
|
+
// PCRE2/ripgrep syntax is not always valid JavaScript regex.
|
|
126
|
+
// Keep source order when a branch cannot be ranked safely.
|
|
127
|
+
branches.push(null);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (branches.length <= 1) return anchors;
|
|
132
|
+
return anchors.map((anchor) => ({
|
|
133
|
+
...anchor,
|
|
134
|
+
priority: branches.findIndex((branch) => branch?.test(anchor.content)),
|
|
135
|
+
})).map((anchor) => ({
|
|
136
|
+
...anchor,
|
|
137
|
+
priority: anchor.priority < 0 ? Number.MAX_SAFE_INTEGER : anchor.priority,
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function selectAnchors(anchors, headLimit, offset) {
|
|
142
|
+
const afterOffset = offset > 0 ? anchors.slice(offset) : anchors;
|
|
143
|
+
if (afterOffset.length === 0) {
|
|
144
|
+
return { selected: [], shown: 0, omitted: 0, nextOffset: offset };
|
|
145
|
+
}
|
|
146
|
+
const shown = headLimit === Infinity ? afterOffset.length : Math.min(headLimit, afterOffset.length);
|
|
147
|
+
const omitted = afterOffset.length - shown;
|
|
148
|
+
if (omitted <= 0 || shown <= 0) {
|
|
149
|
+
return {
|
|
150
|
+
selected: afterOffset.slice(0, shown),
|
|
151
|
+
shown,
|
|
152
|
+
omitted,
|
|
153
|
+
nextOffset: offset + shown,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const headCount = Math.max(1, Math.ceil(shown / 2));
|
|
157
|
+
const tailCount = shown - headCount;
|
|
158
|
+
return {
|
|
159
|
+
selected: [
|
|
160
|
+
...afterOffset.slice(0, headCount),
|
|
161
|
+
...(tailCount > 0 ? afterOffset.slice(afterOffset.length - tailCount) : []),
|
|
162
|
+
],
|
|
163
|
+
shown,
|
|
164
|
+
omitted,
|
|
165
|
+
nextOffset: offset + headCount,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function mergeIntervals(intervals) {
|
|
170
|
+
const sorted = [...intervals].sort((left, right) => left.start - right.start || left.end - right.end);
|
|
171
|
+
const merged = [];
|
|
172
|
+
for (const interval of sorted) {
|
|
173
|
+
const previous = merged.at(-1);
|
|
174
|
+
if (previous && interval.start <= previous.end + 1) {
|
|
175
|
+
previous.end = Math.max(previous.end, interval.end);
|
|
176
|
+
} else {
|
|
177
|
+
merged.push({ ...interval });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return merged;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function readFileWindows(entry, radius, signal) {
|
|
184
|
+
const intervals = mergeIntervals(entry.anchors.map((anchor) => ({
|
|
185
|
+
start: Math.max(1, anchor.lineNo - radius),
|
|
186
|
+
end: anchor.lineNo + radius,
|
|
187
|
+
})));
|
|
188
|
+
const lines = new Map();
|
|
189
|
+
if (intervals.length === 0) return lines;
|
|
190
|
+
const input = createReadStream(entry.absolutePath, { encoding: 'utf8' });
|
|
191
|
+
const reader = createInterface({ input, crlfDelay: Infinity });
|
|
192
|
+
let lineNo = 0;
|
|
193
|
+
let intervalIndex = 0;
|
|
194
|
+
const abort = () => input.destroy(Object.assign(new Error('grep context expansion aborted'), { code: 'ABORT_ERR' }));
|
|
195
|
+
if (signal) {
|
|
196
|
+
if (signal.aborted) abort();
|
|
197
|
+
else signal.addEventListener('abort', abort, { once: true });
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
for await (const line of reader) {
|
|
201
|
+
lineNo++;
|
|
202
|
+
while (intervalIndex < intervals.length && lineNo > intervals[intervalIndex].end) intervalIndex++;
|
|
203
|
+
if (intervalIndex >= intervals.length) {
|
|
204
|
+
input.destroy();
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
const interval = intervals[intervalIndex];
|
|
208
|
+
if (lineNo >= interval.start && lineNo <= interval.end) lines.set(lineNo, line);
|
|
209
|
+
}
|
|
210
|
+
return lines;
|
|
211
|
+
} finally {
|
|
212
|
+
if (signal) signal.removeEventListener('abort', abort);
|
|
213
|
+
reader.close();
|
|
214
|
+
input.destroy();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function readAnchorSources(anchors, radius, signal) {
|
|
219
|
+
const groups = new Map();
|
|
220
|
+
for (const anchor of anchors) {
|
|
221
|
+
if (!groups.has(anchor.absolutePath)) {
|
|
222
|
+
groups.set(anchor.absolutePath, {
|
|
223
|
+
absolutePath: anchor.absolutePath,
|
|
224
|
+
anchors: [],
|
|
225
|
+
lines: new Map(),
|
|
226
|
+
error: null,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
groups.get(anchor.absolutePath).anchors.push(anchor);
|
|
230
|
+
}
|
|
231
|
+
const entries = [...groups.values()];
|
|
232
|
+
let next = 0;
|
|
233
|
+
const worker = async () => {
|
|
234
|
+
while (next < entries.length) {
|
|
235
|
+
const entry = entries[next++];
|
|
236
|
+
try {
|
|
237
|
+
entry.lines = await readFileWindows(entry, radius, signal);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
entry.error = err;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
await Promise.all(Array.from(
|
|
244
|
+
{ length: Math.min(GREP_CONTEXT_READ_CONCURRENCY, entries.length) },
|
|
245
|
+
() => worker(),
|
|
246
|
+
));
|
|
247
|
+
return groups;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function sourceBlock(anchor, source, radius) {
|
|
251
|
+
if (!source || source.error || !source.lines.has(anchor.lineNo)) {
|
|
252
|
+
return {
|
|
253
|
+
path: anchor.path,
|
|
254
|
+
matchLine: anchor.lineNo,
|
|
255
|
+
startLine: anchor.lineNo,
|
|
256
|
+
endLine: anchor.lineNo,
|
|
257
|
+
contents: [anchor.content],
|
|
258
|
+
sourceComplete: false,
|
|
259
|
+
priority: anchor.priority ?? Number.MAX_SAFE_INTEGER,
|
|
260
|
+
order: anchor.order ?? Number.MAX_SAFE_INTEGER,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
let startLine = Math.max(1, anchor.lineNo - radius);
|
|
264
|
+
let endLine = anchor.lineNo + radius;
|
|
265
|
+
while (startLine < anchor.lineNo && !source.lines.has(startLine)) startLine++;
|
|
266
|
+
while (endLine > anchor.lineNo && !source.lines.has(endLine)) endLine--;
|
|
267
|
+
const contents = [];
|
|
268
|
+
for (let lineNo = startLine; lineNo <= endLine; lineNo++) {
|
|
269
|
+
if (!source.lines.has(lineNo)) break;
|
|
270
|
+
contents.push(source.lines.get(lineNo));
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
path: anchor.path,
|
|
274
|
+
matchLine: anchor.lineNo,
|
|
275
|
+
startLine,
|
|
276
|
+
endLine: startLine + contents.length - 1,
|
|
277
|
+
contents,
|
|
278
|
+
sourceComplete: true,
|
|
279
|
+
priority: anchor.priority ?? Number.MAX_SAFE_INTEGER,
|
|
280
|
+
order: anchor.order ?? Number.MAX_SAFE_INTEGER,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function mergeBlocks(blocks) {
|
|
285
|
+
const sorted = [...blocks].sort((left, right) => (
|
|
286
|
+
left.path.localeCompare(right.path)
|
|
287
|
+
|| left.startLine - right.startLine
|
|
288
|
+
|| left.endLine - right.endLine
|
|
289
|
+
));
|
|
290
|
+
const merged = [];
|
|
291
|
+
for (const block of sorted) {
|
|
292
|
+
const previous = merged.at(-1);
|
|
293
|
+
if (previous
|
|
294
|
+
&& previous.path === block.path
|
|
295
|
+
&& block.startLine <= previous.endLine + 1) {
|
|
296
|
+
const overlap = Math.max(0, previous.endLine - block.startLine + 1);
|
|
297
|
+
previous.contents.push(...block.contents.slice(overlap));
|
|
298
|
+
previous.endLine = Math.max(previous.endLine, block.endLine);
|
|
299
|
+
previous.sourceComplete = previous.sourceComplete && block.sourceComplete;
|
|
300
|
+
if (block.priority < previous.priority
|
|
301
|
+
|| (block.priority === previous.priority && block.order < previous.order)) {
|
|
302
|
+
previous.matchLine = block.matchLine;
|
|
303
|
+
previous.priority = block.priority;
|
|
304
|
+
previous.order = block.order;
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
merged.push({ ...block, contents: [...block.contents] });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return merged.sort((left, right) => (
|
|
311
|
+
left.priority - right.priority
|
|
312
|
+
|| left.order - right.order
|
|
313
|
+
));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function pagingNotice({ shown, total, totalKnown, omitted, offset, nextOffset }) {
|
|
317
|
+
const totalStr = totalKnown ? `${total}` : `>=${total}`;
|
|
318
|
+
if (shown === 0) {
|
|
319
|
+
return totalKnown
|
|
320
|
+
? `[Showing 0 of ${totalStr} matches; offset ${offset} past end]`
|
|
321
|
+
: `[Showing 0 of ${totalStr} matches (results partial); offset ${offset} is beyond the streamed window — matches past it may exist. Narrow path/glob/pattern instead of paging deeper.]`;
|
|
322
|
+
}
|
|
323
|
+
return (omitted > 0 || !totalKnown)
|
|
324
|
+
? `\n[Showing ${shown} of ${totalStr} matches${totalKnown ? '' : ' (results partial)'}; pass offset:${nextOffset} for more]`
|
|
325
|
+
: '';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function renderAtRadius(selected, sources, radius, _budget, notice) {
|
|
329
|
+
const blocks = mergeBlocks(selected.map((anchor) => (
|
|
330
|
+
sourceBlock(anchor, sources.get(anchor.absolutePath), radius)
|
|
331
|
+
)));
|
|
332
|
+
const body = blocks.map((block) => (
|
|
333
|
+
`# ${block.path}:${block.matchLine} [lines ${block.startLine}-${block.endLine}]\n${block.contents.join('\n')}`
|
|
334
|
+
)).join('\n');
|
|
335
|
+
const sourceComplete = blocks.every((block) => block.sourceComplete);
|
|
336
|
+
// Header is a format cue only — internal radius/char-budget numbers are
|
|
337
|
+
// harness noise (neither Claude Code nor Codex surface theirs).
|
|
338
|
+
const text = `[Raw source spans; apply_patch context may be copied verbatim]\n${body}${notice}`;
|
|
339
|
+
return { text, sourceComplete, blockCount: blocks.length };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function anchorPriority(left, right) {
|
|
343
|
+
return (left.priority ?? Number.MAX_SAFE_INTEGER) - (right.priority ?? Number.MAX_SAFE_INTEGER)
|
|
344
|
+
|| (left.order ?? Number.MAX_SAFE_INTEGER) - (right.order ?? Number.MAX_SAFE_INTEGER);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function compactAnchorContent(content) {
|
|
348
|
+
const normalized = String(content || '').trim().replace(/\s+/g, ' ');
|
|
349
|
+
return normalized.length <= GREP_COMPACT_ANCHOR_CONTENT_MAX
|
|
350
|
+
? normalized
|
|
351
|
+
: `${normalized.slice(0, GREP_COMPACT_ANCHOR_CONTENT_MAX - 1)}…`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function anchorRangeHint(anchor, radius) {
|
|
355
|
+
const startLine = Math.max(1, anchor.lineNo - radius);
|
|
356
|
+
const endLine = anchor.lineNo + radius;
|
|
357
|
+
return `lines ${startLine}-${endLine}`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function renderFocusedContext(selected, sources, radius, _budget, notice) {
|
|
361
|
+
const ordered = [...selected].sort(anchorPriority);
|
|
362
|
+
const rankedBlocks = mergeBlocks(ordered.map((anchor) => (
|
|
363
|
+
sourceBlock(anchor, sources.get(anchor.absolutePath), radius)
|
|
364
|
+
)));
|
|
365
|
+
const rawBlocks = rankedBlocks.slice(0, GREP_FOCUSED_RAW_BLOCKS);
|
|
366
|
+
const covered = (anchor) => rawBlocks.some((block) => (
|
|
367
|
+
anchor.path === block.path
|
|
368
|
+
&& anchor.lineNo >= block.startLine
|
|
369
|
+
&& anchor.lineNo <= block.endLine
|
|
370
|
+
));
|
|
371
|
+
const compact = ordered.filter((anchor) => !covered(anchor));
|
|
372
|
+
const raw = rawBlocks.map((block) => (
|
|
373
|
+
`# ${block.path}:${block.matchLine} [lines ${block.startLine}-${block.endLine}]\n${block.contents.join('\n')}`
|
|
374
|
+
)).join('\n');
|
|
375
|
+
const anchors = compact.length
|
|
376
|
+
? `\n# Additional matches\n${compact.map((anchor) => (
|
|
377
|
+
`${anchor.path}:${anchor.lineNo}:${compactAnchorContent(anchor.content)} [${anchorRangeHint(anchor, radius)}]`
|
|
378
|
+
)).join('\n')}`
|
|
379
|
+
: '';
|
|
380
|
+
const header = compact.length
|
|
381
|
+
? `[Top ${rawBlocks.length} of ${ordered.length}; remaining as path:line anchors]`
|
|
382
|
+
: null;
|
|
383
|
+
const text = `${header ? `${header}\n` : ''}${raw}${anchors}${notice}`;
|
|
384
|
+
return {
|
|
385
|
+
text,
|
|
386
|
+
sourceComplete: rawBlocks.every((block) => block.sourceComplete),
|
|
387
|
+
blockCount: rawBlocks.length,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function expandGrepAnchorContextOutput({
|
|
392
|
+
allLines,
|
|
393
|
+
workDir,
|
|
394
|
+
rgSpawnCwd,
|
|
395
|
+
grepResolvedPath,
|
|
396
|
+
searchPath,
|
|
397
|
+
outputMode,
|
|
398
|
+
filenameOmitted,
|
|
399
|
+
headLimit,
|
|
400
|
+
offset,
|
|
401
|
+
totalKnown = true,
|
|
402
|
+
requestedContext,
|
|
403
|
+
maxContext,
|
|
404
|
+
patterns = [],
|
|
405
|
+
caseInsensitive = false,
|
|
406
|
+
charBudget = GREP_CONTEXT_CHAR_BUDGET_DEFAULT,
|
|
407
|
+
signal,
|
|
408
|
+
}) {
|
|
409
|
+
const anchors = rankAnchors(parseAnchors(allLines, {
|
|
410
|
+
workDir,
|
|
411
|
+
rgSpawnCwd,
|
|
412
|
+
grepResolvedPath,
|
|
413
|
+
searchPath,
|
|
414
|
+
outputMode,
|
|
415
|
+
filenameOmitted,
|
|
416
|
+
}), patterns, caseInsensitive);
|
|
417
|
+
const total = anchors.length;
|
|
418
|
+
if (total === 0) return { text: '', total: 0, shown: 0, omitted: 0, sourceComplete: true };
|
|
419
|
+
const window = selectAnchors(anchors, headLimit, offset);
|
|
420
|
+
let notice = pagingNotice({
|
|
421
|
+
...window,
|
|
422
|
+
total,
|
|
423
|
+
totalKnown,
|
|
424
|
+
offset,
|
|
425
|
+
});
|
|
426
|
+
if (window.selected.length === 0) {
|
|
427
|
+
return {
|
|
428
|
+
text: notice,
|
|
429
|
+
total,
|
|
430
|
+
shown: 0,
|
|
431
|
+
omitted: 0,
|
|
432
|
+
sourceComplete: true,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const requested = Math.max(0, Math.floor(Number(requestedContext) || 0));
|
|
436
|
+
const target = Math.max(requested, Math.max(0, Math.floor(Number(maxContext) || 0)));
|
|
437
|
+
const budget = Math.max(512, Math.floor(Number(charBudget) || GREP_CONTEXT_CHAR_BUDGET_DEFAULT));
|
|
438
|
+
const sources = await readAnchorSources(window.selected, target, signal);
|
|
439
|
+
let selected = window.selected;
|
|
440
|
+
let shown = window.shown;
|
|
441
|
+
let omitted = window.omitted;
|
|
442
|
+
let radius = target;
|
|
443
|
+
let rendered = renderAtRadius(selected, sources, radius, budget, notice);
|
|
444
|
+
// Progressive disclosure: one or two source clusters that fit are most
|
|
445
|
+
// useful as patch-ready raw text. Broad searches instead expand up to three
|
|
446
|
+
// high-priority source clusters and retain the rest as compact path:line
|
|
447
|
+
// anchors with neutral range metadata. This avoids paying 25 lines for
|
|
448
|
+
// every match without nudging the model into unnecessary follow-up reads.
|
|
449
|
+
const sparseRaw = rendered.blockCount <= 2 && rendered.text.length <= budget;
|
|
450
|
+
if (!sparseRaw) {
|
|
451
|
+
radius = Math.min(target, GREP_FOCUSED_CONTEXT_RADIUS);
|
|
452
|
+
rendered = renderFocusedContext(selected, sources, radius, budget, notice);
|
|
453
|
+
while (rendered.text.length > budget && radius > 0) {
|
|
454
|
+
radius--;
|
|
455
|
+
rendered = renderFocusedContext(selected, sources, radius, budget, notice);
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
return {
|
|
459
|
+
...rendered,
|
|
460
|
+
total,
|
|
461
|
+
shown,
|
|
462
|
+
omitted,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
while (rendered.text.length > budget && selected.length > 1) {
|
|
466
|
+
selected = [...selected].sort(anchorPriority).slice(0, -1);
|
|
467
|
+
shown = selected.length;
|
|
468
|
+
omitted = Math.max(0, total - offset - shown);
|
|
469
|
+
notice = pagingNotice({
|
|
470
|
+
shown,
|
|
471
|
+
total,
|
|
472
|
+
totalKnown,
|
|
473
|
+
omitted,
|
|
474
|
+
offset,
|
|
475
|
+
nextOffset: offset + shown,
|
|
476
|
+
});
|
|
477
|
+
rendered = renderFocusedContext(
|
|
478
|
+
selected,
|
|
479
|
+
sources,
|
|
480
|
+
radius,
|
|
481
|
+
budget,
|
|
482
|
+
notice,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
...rendered,
|
|
487
|
+
total,
|
|
488
|
+
shown,
|
|
489
|
+
omitted,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
import { normalizeOutputPath } from '../path-utils.mjs';
|
|
6
6
|
import {
|
|
7
7
|
groupGrepContentByFile,
|
|
8
|
+
parseGrepContextHeader,
|
|
8
9
|
splitGrepLineNumberOnlyPrefix,
|
|
9
10
|
splitGrepLinePrefix,
|
|
10
11
|
} from '../grep-formatting.mjs';
|
|
12
|
+
import { relativePathPrefix } from '../search-path-diagnostics.mjs';
|
|
11
13
|
import { relativeGrepLine } from './search-input-helpers.mjs';
|
|
12
14
|
|
|
13
15
|
export function grepMissingPatternMessage() {
|
|
@@ -22,30 +24,84 @@ export function globMissingPatternMessage() {
|
|
|
22
24
|
// In context mode (explicit -A/-B/-C or content_with_context auto), head_limit
|
|
23
25
|
// and offset count MATCH BLOCKS, not raw output lines, and truncation keeps a
|
|
24
26
|
// head+tail slice with a middle marker instead of dropping the tail.
|
|
25
|
-
function
|
|
27
|
+
function grepContextSourceLine(line, filenameOmitted, fallbackPath = '') {
|
|
26
28
|
if (filenameOmitted) {
|
|
27
29
|
const p = splitGrepLineNumberOnlyPrefix(line);
|
|
28
|
-
return p
|
|
30
|
+
return p ? {
|
|
31
|
+
path: fallbackPath,
|
|
32
|
+
lineNo: p.lineNo,
|
|
33
|
+
delimiter: p.delimiter,
|
|
34
|
+
content: String(line).slice(p.markerEnd),
|
|
35
|
+
} : null;
|
|
29
36
|
}
|
|
30
37
|
const s = splitGrepLinePrefix(line);
|
|
31
|
-
return s
|
|
38
|
+
return s ? {
|
|
39
|
+
path: s.path,
|
|
40
|
+
lineNo: s.lineNo,
|
|
41
|
+
delimiter: s.delimiter,
|
|
42
|
+
content: String(line).slice(s.markerEnd),
|
|
43
|
+
} : null;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
|
-
function
|
|
46
|
+
function grepBlockMatchAnchor(line, filenameOmitted, fallbackPath) {
|
|
47
|
+
const parsed = grepContextSourceLine(line, filenameOmitted, fallbackPath);
|
|
48
|
+
return parsed && parsed.delimiter === ':' ? `${parsed.path}\0${parsed.lineNo}` : '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseRenderableGrepContextBlock(block, filenameOmitted, fallbackPath) {
|
|
52
|
+
const parsed = block.lines.map((line) => grepContextSourceLine(line, filenameOmitted, fallbackPath));
|
|
53
|
+
const contiguous = parsed.length > 0
|
|
54
|
+
&& parsed.every((entry, index) => entry
|
|
55
|
+
&& entry.path
|
|
56
|
+
&& entry.path === parsed[0].path
|
|
57
|
+
&& (index === 0 || entry.lineNo === parsed[index - 1].lineNo + 1));
|
|
58
|
+
const match = contiguous ? parsed.find((entry) => entry.delimiter === ':') : null;
|
|
59
|
+
if (!match) return { raw: block.lines.join('\n') };
|
|
60
|
+
return {
|
|
61
|
+
path: match.path,
|
|
62
|
+
matchLine: match.lineNo,
|
|
63
|
+
startLine: parsed[0].lineNo,
|
|
64
|
+
endLine: parsed.at(-1).lineNo,
|
|
65
|
+
contents: parsed.map((entry) => entry.content),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderGrepContextBlocks(blocks, filenameOmitted, fallbackPath) {
|
|
70
|
+
const merged = [];
|
|
71
|
+
for (const block of blocks) {
|
|
72
|
+
const current = parseRenderableGrepContextBlock(block, filenameOmitted, fallbackPath);
|
|
73
|
+
const previous = merged.at(-1);
|
|
74
|
+
if (previous?.path
|
|
75
|
+
&& current.path === previous.path
|
|
76
|
+
&& current.startLine <= previous.endLine + 1) {
|
|
77
|
+
const overlap = Math.max(0, previous.endLine - current.startLine + 1);
|
|
78
|
+
previous.contents.push(...current.contents.slice(overlap));
|
|
79
|
+
previous.endLine = Math.max(previous.endLine, current.endLine);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
merged.push(current);
|
|
83
|
+
}
|
|
84
|
+
return merged.map((block) => {
|
|
85
|
+
if (!block.path) return block.raw;
|
|
86
|
+
return `# ${block.path}:${block.matchLine} [lines ${block.startLine}-${block.endLine}]\n${block.contents.join('\n')}`;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseGrepContextBlocks(lines, filenameOmitted, fallbackPath) {
|
|
35
91
|
const blocks = [];
|
|
36
92
|
let pending = [];
|
|
37
93
|
let i = 0;
|
|
38
94
|
while (i < lines.length) {
|
|
39
95
|
const line = lines[i];
|
|
40
96
|
if (line === '--') { pending = []; i++; continue; }
|
|
41
|
-
const anchor = grepBlockMatchAnchor(line, filenameOmitted);
|
|
97
|
+
const anchor = grepBlockMatchAnchor(line, filenameOmitted, fallbackPath);
|
|
42
98
|
if (anchor) {
|
|
43
99
|
const blockLines = pending.concat([line]);
|
|
44
100
|
pending = [];
|
|
45
101
|
i++;
|
|
46
102
|
while (i < lines.length) {
|
|
47
103
|
const next = lines[i];
|
|
48
|
-
if (next === '--' || grepBlockMatchAnchor(next, filenameOmitted)) break;
|
|
104
|
+
if (next === '--' || grepBlockMatchAnchor(next, filenameOmitted, fallbackPath)) break;
|
|
49
105
|
blockLines.push(next);
|
|
50
106
|
i++;
|
|
51
107
|
}
|
|
@@ -58,9 +114,12 @@ function parseGrepContextBlocks(lines, filenameOmitted) {
|
|
|
58
114
|
return blocks;
|
|
59
115
|
}
|
|
60
116
|
|
|
61
|
-
export function formatGrepContextOutput({ allLines, workDir, outputMode, filenameOmitted, headLimit, offset, totalKnown = true }) {
|
|
117
|
+
export function formatGrepContextOutput({ allLines, workDir, outputMode, filenameOmitted, headLimit, offset, searchPath, totalKnown = true }) {
|
|
62
118
|
const norm = allLines.map((l) => (l === '--' ? '--' : relativeGrepLine(l, workDir, false, outputMode, filenameOmitted)));
|
|
63
|
-
const
|
|
119
|
+
const fallbackPath = filenameOmitted
|
|
120
|
+
? relativePathPrefix(normalizeOutputPath(searchPath), workDir)
|
|
121
|
+
: '';
|
|
122
|
+
const blocks = parseGrepContextBlocks(norm, filenameOmitted, fallbackPath);
|
|
64
123
|
const total = blocks.length;
|
|
65
124
|
if (total === 0) return { text: '', total: 0, shown: 0, omitted: 0 };
|
|
66
125
|
// Finding 2/3: denominator is the PRE-offset grand total; on a partial rg
|
|
@@ -78,7 +137,7 @@ export function formatGrepContextOutput({ allLines, workDir, outputMode, filenam
|
|
|
78
137
|
}
|
|
79
138
|
const shown = headLimit === Infinity ? afterOffset.length : Math.min(headLimit, afterOffset.length);
|
|
80
139
|
const omitted = afterOffset.length - shown;
|
|
81
|
-
const render = (arr) => arr
|
|
140
|
+
const render = (arr) => renderGrepContextBlocks(arr, filenameOmitted, fallbackPath);
|
|
82
141
|
let segments;
|
|
83
142
|
let nextOffset = offset + shown;
|
|
84
143
|
if (omitted > 0 && shown > 0) {
|
|
@@ -99,16 +158,35 @@ export function formatGrepContextOutput({ allLines, workDir, outputMode, filenam
|
|
|
99
158
|
const notice = (omitted > 0 || !totalKnown)
|
|
100
159
|
? `\n[Showing ${shown} of ${totalStr} matches${totalKnown ? '' : ' (results partial)'}; pass offset:${nextOffset} for more]`
|
|
101
160
|
: '';
|
|
102
|
-
return {
|
|
161
|
+
return {
|
|
162
|
+
text: `${segments.join('\n')}${notice}`,
|
|
163
|
+
total,
|
|
164
|
+
shown,
|
|
165
|
+
omitted,
|
|
166
|
+
};
|
|
103
167
|
}
|
|
104
168
|
|
|
105
|
-
// Part 1: drop
|
|
106
|
-
// a pattern[] fan-out.
|
|
169
|
+
// Part 1: drop a numbered match line or patch-ready context block already
|
|
170
|
+
// emitted by an earlier pattern in a pattern[] fan-out.
|
|
107
171
|
export function dedupeFanoutMatchLines(body, seen) {
|
|
108
172
|
const text = String(body);
|
|
109
173
|
if (/^Error:/.test(text)) return text;
|
|
110
174
|
const out = [];
|
|
111
|
-
|
|
175
|
+
const lines = text.split('\n');
|
|
176
|
+
for (let i = 0; i < lines.length; i++) {
|
|
177
|
+
const line = lines[i];
|
|
178
|
+
const header = parseGrepContextHeader(line);
|
|
179
|
+
if (header) {
|
|
180
|
+
const key = `${header.path}\0${header.matchLine}`;
|
|
181
|
+
const duplicate = seen.has(key);
|
|
182
|
+
seen.add(key);
|
|
183
|
+
if (!duplicate) out.push(line);
|
|
184
|
+
for (let sourceIndex = 0; sourceIndex < header.sourceLineCount && i + 1 < lines.length; sourceIndex++) {
|
|
185
|
+
i++;
|
|
186
|
+
if (!duplicate) out.push(lines[i]);
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
112
190
|
const s = splitGrepLinePrefix(line);
|
|
113
191
|
if (s && s.delimiter === ':') {
|
|
114
192
|
const key = `${s.path}\0${s.lineNo}`;
|