@oh-my-pi/pi-coding-agent 14.7.8 → 14.8.1
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 +13 -0
- package/README.md +1 -0
- package/package.json +7 -7
- package/src/commit/agentic/index.ts +2 -1
- package/src/config/model-registry.ts +18 -10
- package/src/edit/file-read-cache.ts +95 -0
- package/src/edit/index.ts +1 -0
- package/src/edit/modes/hashline.ts +137 -2
- package/src/extensibility/extensions/loader.ts +5 -2
- package/src/extensibility/extensions/types.ts +10 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +166 -0
- package/src/extensibility/plugins/loader.ts +3 -7
- package/src/internal-urls/docs-index.generated.ts +2 -1
- package/src/modes/components/model-selector.ts +22 -2
- package/src/modes/components/status-line/segments.ts +2 -2
- package/src/modes/components/status-line-segment-editor.ts +2 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/mcp-command-controller.ts +4 -12
- package/src/modes/interactive-mode.ts +41 -2
- package/src/modes/prompt-action-autocomplete.ts +3 -0
- package/src/modes/types.ts +4 -1
- package/src/prompts/tools/eval.md +1 -1
- package/src/prompts/tools/hashline.md +17 -3
- package/src/tools/eval.ts +10 -0
- package/src/tools/fs-cache-invalidation.ts +2 -2
- package/src/tools/index.ts +6 -0
- package/src/tools/read.ts +68 -11
- package/src/tools/search.ts +10 -4
- package/src/utils/git.ts +10 -4
package/src/tools/read.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
8
8
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { getRemoteDir, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { type Static, Type } from "@sinclair/typebox";
|
|
11
|
+
import { getFileReadCache } from "../edit/file-read-cache";
|
|
11
12
|
import { formatHashLine, formatHashLines, formatLineHash, HL_BODY_SEP } from "../edit/line-hash";
|
|
12
13
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
13
14
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -163,6 +164,33 @@ function countTextLines(text: string): number {
|
|
|
163
164
|
}
|
|
164
165
|
const READ_CHUNK_SIZE = 8 * 1024;
|
|
165
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Number of unanchored context lines to include before/after a user-requested
|
|
169
|
+
* range. Anchor-stale failures are heavily concentrated on edits whose anchors
|
|
170
|
+
* land just outside the most recent read window — a few lines of pre-anchored
|
|
171
|
+
* context covers off-by-one anchor selection without much cost.
|
|
172
|
+
*/
|
|
173
|
+
const RANGE_CONTEXT_LINES = 3;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Expand a [start, end) range with ±RANGE_CONTEXT_LINES context lines on the
|
|
177
|
+
* sides where the user actually constrained the range. A start of 0 (no
|
|
178
|
+
* explicit offset) does not get leading context — that's already an open-ended
|
|
179
|
+
* read from the top.
|
|
180
|
+
*/
|
|
181
|
+
function expandRangeWithContext(
|
|
182
|
+
requestedStart: number,
|
|
183
|
+
requestedEnd: number,
|
|
184
|
+
totalLines: number,
|
|
185
|
+
expandStart: boolean,
|
|
186
|
+
expandEnd: boolean,
|
|
187
|
+
): { startLine: number; endLine: number } {
|
|
188
|
+
return {
|
|
189
|
+
startLine: expandStart ? Math.max(0, requestedStart - RANGE_CONTEXT_LINES) : requestedStart,
|
|
190
|
+
endLine: expandEnd ? Math.min(totalLines, requestedEnd + RANGE_CONTEXT_LINES) : requestedEnd,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
166
194
|
async function streamLinesFromFile(
|
|
167
195
|
filePath: string,
|
|
168
196
|
startLine: number,
|
|
@@ -645,9 +673,26 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
645
673
|
const details = options.details ?? {};
|
|
646
674
|
const allLines = text.split("\n");
|
|
647
675
|
const totalLines = allLines.length;
|
|
648
|
-
|
|
649
|
-
|
|
676
|
+
// User-requested 0-indexed range start. Lines BEFORE this are leading
|
|
677
|
+
// context (added below if offset is explicit).
|
|
678
|
+
const requestedStart = offset ? Math.max(0, offset - 1) : 0;
|
|
650
679
|
const ignoreResultLimits = options.ignoreResultLimits ?? false;
|
|
680
|
+
const requestedEnd =
|
|
681
|
+
limit !== undefined && !ignoreResultLimits
|
|
682
|
+
? Math.min(requestedStart + limit, allLines.length)
|
|
683
|
+
: allLines.length;
|
|
684
|
+
// Expand only on sides the user actually constrained: leading context
|
|
685
|
+
// when offset>1, trailing context when a finite limit was set.
|
|
686
|
+
const expanded = expandRangeWithContext(
|
|
687
|
+
requestedStart,
|
|
688
|
+
requestedEnd,
|
|
689
|
+
allLines.length,
|
|
690
|
+
offset !== undefined && offset > 1,
|
|
691
|
+
limit !== undefined && !ignoreResultLimits,
|
|
692
|
+
);
|
|
693
|
+
const startLine = expanded.startLine;
|
|
694
|
+
const endLineExpanded = expanded.endLine;
|
|
695
|
+
const startLineDisplay = startLine + 1;
|
|
651
696
|
|
|
652
697
|
const resultBuilder = toolResult(details);
|
|
653
698
|
if (options.sourcePath) {
|
|
@@ -660,20 +705,19 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
660
705
|
resultBuilder.sourceInternal(options.sourceInternal);
|
|
661
706
|
}
|
|
662
707
|
|
|
663
|
-
if (
|
|
708
|
+
if (requestedStart >= allLines.length) {
|
|
664
709
|
const suggestion =
|
|
665
710
|
allLines.length === 0
|
|
666
711
|
? `The ${options.entityLabel} is empty.`
|
|
667
712
|
: `Use :1 to read from the start, or :${allLines.length} to read the last line.`;
|
|
668
713
|
return resultBuilder
|
|
669
714
|
.text(
|
|
670
|
-
`Line ${
|
|
715
|
+
`Line ${requestedStart + 1} is beyond end of ${options.entityLabel} (${allLines.length} lines total). ${suggestion}`,
|
|
671
716
|
)
|
|
672
717
|
.done();
|
|
673
718
|
}
|
|
674
719
|
|
|
675
|
-
const endLine =
|
|
676
|
-
limit !== undefined && !ignoreResultLimits ? Math.min(startLine + limit, allLines.length) : allLines.length;
|
|
720
|
+
const endLine = endLineExpanded;
|
|
677
721
|
const selectedContent = allLines.slice(startLine, endLine).join("\n");
|
|
678
722
|
const userLimitedLines = limit !== undefined && !ignoreResultLimits ? endLine - startLine : undefined;
|
|
679
723
|
const truncation = ignoreResultLimits ? noTruncResult(selectedContent) : truncateHead(selectedContent);
|
|
@@ -1318,13 +1362,20 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1318
1362
|
if (!content) {
|
|
1319
1363
|
// Raw text or line-range mode
|
|
1320
1364
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1321
|
-
|
|
1365
|
+
// User-requested 0-indexed range start. Lines BEFORE this become
|
|
1366
|
+
// leading context (added below if offset is explicit).
|
|
1367
|
+
const requestedStart = offset ? Math.max(0, offset - 1) : 0;
|
|
1368
|
+
const expandStart = offset !== undefined && offset > 1;
|
|
1369
|
+
const expandEnd = limit !== undefined;
|
|
1370
|
+
const leadingContext = expandStart ? Math.min(requestedStart, RANGE_CONTEXT_LINES) : 0;
|
|
1371
|
+
const trailingContext = expandEnd ? RANGE_CONTEXT_LINES : 0;
|
|
1372
|
+
const startLine = requestedStart - leadingContext;
|
|
1322
1373
|
const startLineDisplay = startLine + 1;
|
|
1323
1374
|
|
|
1324
1375
|
const DEFAULT_LIMIT = this.#defaultLimit;
|
|
1325
1376
|
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
1326
|
-
const maxLinesToCollect = Math.min(effectiveLimit, DEFAULT_MAX_LINES);
|
|
1327
|
-
const selectedLineLimit = effectiveLimit;
|
|
1377
|
+
const maxLinesToCollect = Math.min(effectiveLimit + leadingContext + trailingContext, DEFAULT_MAX_LINES);
|
|
1378
|
+
const selectedLineLimit = effectiveLimit + leadingContext + trailingContext;
|
|
1328
1379
|
// Scale byte budget with line limit so the configured line count actually fits.
|
|
1329
1380
|
// Assume ~512 bytes/line average; never go below the shared default.
|
|
1330
1381
|
const maxBytesForRead = Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512);
|
|
@@ -1348,13 +1399,15 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1348
1399
|
} = streamResult;
|
|
1349
1400
|
|
|
1350
1401
|
// Check if offset is out of bounds - return graceful message instead of throwing
|
|
1351
|
-
if (
|
|
1402
|
+
if (requestedStart >= totalFileLines) {
|
|
1352
1403
|
const suggestion =
|
|
1353
1404
|
totalFileLines === 0
|
|
1354
1405
|
? "The file is empty."
|
|
1355
1406
|
: `Use :1 to read from the start, or :${totalFileLines} to read the last line.`;
|
|
1356
1407
|
return toolResult<ReadToolDetails>({ resolvedPath: absolutePath, suffixResolution })
|
|
1357
|
-
.text(
|
|
1408
|
+
.text(
|
|
1409
|
+
`Line ${requestedStart + 1} is beyond end of file (${totalFileLines} lines total). ${suggestion}`,
|
|
1410
|
+
)
|
|
1358
1411
|
.done();
|
|
1359
1412
|
}
|
|
1360
1413
|
|
|
@@ -1378,6 +1431,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1378
1431
|
firstLineExceedsLimit,
|
|
1379
1432
|
};
|
|
1380
1433
|
|
|
1434
|
+
if (collectedLines.length > 0 && !firstLineExceedsLimit) {
|
|
1435
|
+
getFileReadCache(this.session).recordContiguous(absolutePath, startLineDisplay, collectedLines);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1381
1438
|
const isRawMode = parsed.kind === "raw";
|
|
1382
1439
|
const shouldAddHashLines = !isRawMode && displayMode.hashLines;
|
|
1383
1440
|
const shouldAddLineNumbers = isRawMode ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
package/src/tools/search.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
9
|
+
import { getFileReadCache } from "../edit/file-read-cache";
|
|
9
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
11
|
import type { Theme } from "../modes/theme/theme";
|
|
11
12
|
import searchDescription from "../prompts/tools/search.md" with { type: "text" };
|
|
@@ -345,27 +346,32 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
345
346
|
}
|
|
346
347
|
return nextWidth;
|
|
347
348
|
}, 0);
|
|
349
|
+
const cacheEntries: Array<readonly [number, string]> = [];
|
|
348
350
|
for (const match of fileMatches) {
|
|
349
|
-
const pushLine = (lineNumber: number, line: string, isMatch: boolean) => {
|
|
351
|
+
const pushLine = (lineNumber: number, line: string, isMatch: boolean, recordable: boolean) => {
|
|
350
352
|
modelOut.push(formatMatchLine(lineNumber, line, isMatch, { useHashLines }));
|
|
351
353
|
displayOut.push(formatCodeFrameLine(isMatch ? "*" : " ", lineNumber, line, lineNumberWidth));
|
|
354
|
+
if (recordable) cacheEntries.push([lineNumber, line] as const);
|
|
352
355
|
};
|
|
353
356
|
if (match.contextBefore) {
|
|
354
357
|
for (const ctx of match.contextBefore) {
|
|
355
|
-
pushLine(ctx.lineNumber, ctx.line, false);
|
|
358
|
+
pushLine(ctx.lineNumber, ctx.line, false, true);
|
|
356
359
|
}
|
|
357
360
|
}
|
|
358
|
-
pushLine(match.lineNumber, match.line, true);
|
|
361
|
+
pushLine(match.lineNumber, match.line, true, !match.truncated);
|
|
359
362
|
if (match.truncated) {
|
|
360
363
|
linesTruncated = true;
|
|
361
364
|
}
|
|
362
365
|
if (match.contextAfter) {
|
|
363
366
|
for (const ctx of match.contextAfter) {
|
|
364
|
-
pushLine(ctx.lineNumber, ctx.line, false);
|
|
367
|
+
pushLine(ctx.lineNumber, ctx.line, false, true);
|
|
365
368
|
}
|
|
366
369
|
}
|
|
367
370
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
368
371
|
}
|
|
372
|
+
if (cacheEntries.length > 0) {
|
|
373
|
+
getFileReadCache(this.session).recordSparse(path.resolve(searchPath, relativePath), cacheEntries);
|
|
374
|
+
}
|
|
369
375
|
return { model: modelOut, display: displayOut };
|
|
370
376
|
};
|
|
371
377
|
if (isDirectory) {
|
package/src/utils/git.ts
CHANGED
|
@@ -40,6 +40,12 @@ export type HunkSelection = {
|
|
|
40
40
|
hunks: { type: "all" } | { type: "indices"; indices: number[] } | { type: "lines"; start: number; end: number };
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
export interface StageHunksOptions {
|
|
44
|
+
readonly diffCached?: boolean;
|
|
45
|
+
readonly rawDiff?: string;
|
|
46
|
+
readonly signal?: AbortSignal;
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
export interface DiffOptions {
|
|
44
50
|
readonly allowFailure?: boolean;
|
|
45
51
|
readonly base?: string;
|
|
@@ -803,10 +809,10 @@ export const stage = {
|
|
|
803
809
|
await runEffect(cwd, args, { signal });
|
|
804
810
|
},
|
|
805
811
|
|
|
806
|
-
/** Selectively stage hunks from the working tree diff. */
|
|
807
|
-
async hunks(cwd: string, selections: HunkSelection[],
|
|
812
|
+
/** Selectively stage hunks from the provided diff or the current working tree diff. */
|
|
813
|
+
async hunks(cwd: string, selections: HunkSelection[], options: StageHunksOptions = {}): Promise<void> {
|
|
808
814
|
if (selections.length === 0) return;
|
|
809
|
-
const rawDiff = await diff(cwd, { cached:
|
|
815
|
+
const rawDiff = options.rawDiff ?? (await diff(cwd, { cached: options.diffCached, signal: options.signal }));
|
|
810
816
|
const fileDiffs = parseFileDiffs(rawDiff);
|
|
811
817
|
const fileDiffMap = new Map(fileDiffs.map(entry => [entry.filename, entry]));
|
|
812
818
|
const patchParts: string[] = [];
|
|
@@ -833,7 +839,7 @@ export const stage = {
|
|
|
833
839
|
|
|
834
840
|
const patchText = patch.join(patchParts);
|
|
835
841
|
if (!patchText.trim()) return;
|
|
836
|
-
await patch.applyText(cwd, patchText, { cached: true, signal });
|
|
842
|
+
await patch.applyText(cwd, patchText, { cached: true, signal: options.signal });
|
|
837
843
|
},
|
|
838
844
|
|
|
839
845
|
/** Unstage files. Empty array unstages all (`git reset`). */
|