@oh-my-pi/pi-coding-agent 15.5.7 → 15.5.9
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 +53 -1
- package/dist/types/cli/auth-gateway-cli.d.ts +8 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +10 -10
- package/dist/types/edit/file-snapshot-store.d.ts +9 -6
- package/dist/types/edit/hashline/diff.d.ts +4 -5
- package/dist/types/edit/streaming.d.ts +2 -1
- package/dist/types/eval/py/index.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/vault-protocol.d.ts +93 -0
- package/dist/types/mcp/transports/http.d.ts +9 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +3 -1
- package/dist/types/tools/match-line-format.d.ts +2 -2
- package/dist/types/tools/render-utils.d.ts +3 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +2 -0
- package/package.json +8 -8
- package/src/cli/args.ts +2 -0
- package/src/cli/auth-broker-cli.ts +2 -1
- package/src/cli/auth-gateway-cli.ts +210 -9
- package/src/commands/auth-gateway.ts +7 -1
- package/src/config/settings-schema.ts +12 -11
- package/src/edit/file-snapshot-store.ts +9 -6
- package/src/edit/hashline/diff.ts +26 -13
- package/src/edit/hashline/execute.ts +13 -9
- package/src/edit/renderer.ts +9 -9
- package/src/edit/streaming.ts +4 -6
- package/src/eval/py/index.ts +1 -1
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/shared-events.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +2 -0
- package/src/internal-urls/vault-protocol.ts +936 -0
- package/src/main.ts +1 -2
- package/src/mcp/transports/http.ts +29 -2
- package/src/modes/components/tool-execution.ts +6 -4
- package/src/modes/controllers/event-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +10 -2
- package/src/modes/utils/ui-helpers.ts +2 -1
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/read.md +3 -3
- package/src/prompts/tools/search.md +1 -1
- package/src/sdk.ts +26 -1
- package/src/session/agent-session.ts +82 -11
- package/src/system-prompt.ts +2 -0
- package/src/tools/ast-edit.ts +10 -7
- package/src/tools/ast-grep.ts +12 -11
- package/src/tools/eval.ts +28 -3
- package/src/tools/match-line-format.ts +2 -2
- package/src/tools/path-utils.ts +2 -0
- package/src/tools/plan-mode-guard.ts +6 -1
- package/src/tools/read.ts +70 -55
- package/src/tools/render-utils.ts +15 -0
- package/src/tools/search.ts +12 -12
- package/src/tools/write.ts +61 -6
- package/src/utils/file-mentions.ts +11 -5
- package/src/web/search/providers/codex.ts +2 -1
package/src/tools/ast-grep.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
1
3
|
import * as path from "node:path";
|
|
2
|
-
import {
|
|
4
|
+
import { formatHashlineHeader } from "@oh-my-pi/hashline";
|
|
3
5
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
4
6
|
import { type AstFindMatch, astGrep } from "@oh-my-pi/pi-natives";
|
|
5
7
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
@@ -219,14 +221,14 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
219
221
|
}
|
|
220
222
|
|
|
221
223
|
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
|
222
|
-
const hashContexts = new Map<string, { absolutePath: string;
|
|
224
|
+
const hashContexts = new Map<string, { absolutePath: string; tag?: string }>();
|
|
225
|
+
const snapshotStore = useHashLines ? getFileSnapshotStore(this.session) : undefined;
|
|
223
226
|
if (useHashLines) {
|
|
224
227
|
for (const relativePath of fileList) {
|
|
225
228
|
const absolutePath = path.resolve(this.session.cwd, relativePath);
|
|
226
229
|
try {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
hashContexts.set(relativePath, { absolutePath, fileHash });
|
|
230
|
+
await access(absolutePath, constants.R_OK);
|
|
231
|
+
hashContexts.set(relativePath, { absolutePath });
|
|
230
232
|
} catch {
|
|
231
233
|
// Best-effort: if a file disappears between ast-grep and rendering, emit plain line output.
|
|
232
234
|
}
|
|
@@ -268,9 +270,8 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
268
270
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
269
271
|
}
|
|
270
272
|
if (hashContext && cacheEntries.length > 0) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
});
|
|
273
|
+
const tag = snapshotStore?.recordSparse(hashContext.absolutePath, cacheEntries);
|
|
274
|
+
if (tag) hashContext.tag = tag;
|
|
274
275
|
}
|
|
275
276
|
return { model: modelOut, display: displayOut };
|
|
276
277
|
};
|
|
@@ -282,7 +283,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
282
283
|
return {
|
|
283
284
|
modelLines: rendered.model,
|
|
284
285
|
displayLines: rendered.display,
|
|
285
|
-
headerSuffix: hashContext ? `#${hashContext.
|
|
286
|
+
headerSuffix: hashContext?.tag ? `#${hashContext.tag}` : "",
|
|
286
287
|
skip: rendered.model.length === 0,
|
|
287
288
|
};
|
|
288
289
|
});
|
|
@@ -297,8 +298,8 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
297
298
|
displayLines.push("");
|
|
298
299
|
}
|
|
299
300
|
const hashContext = hashContexts.get(relativePath);
|
|
300
|
-
if (hashContext) {
|
|
301
|
-
outputLines.push(formatHashlineHeader(relativePath, hashContext.
|
|
301
|
+
if (hashContext?.tag) {
|
|
302
|
+
outputLines.push(formatHashlineHeader(relativePath, hashContext.tag));
|
|
302
303
|
}
|
|
303
304
|
outputLines.push(...rendered.model);
|
|
304
305
|
displayLines.push(...rendered.display);
|
package/src/tools/eval.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
|
14
14
|
import evalDescription from "../prompts/tools/eval.md" with { type: "text" };
|
|
15
15
|
import { DEFAULT_MAX_BYTES, OutputSink, type OutputSummary, TailBuffer } from "../session/streaming-output";
|
|
16
16
|
import { getTreeBranch, getTreeContinuePrefix, renderCodeCell } from "../tui";
|
|
17
|
+
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
17
18
|
import { resolveEvalBackends, type ToolSession } from ".";
|
|
18
19
|
import { truncateForPrompt } from "./approval";
|
|
19
20
|
import {
|
|
@@ -403,6 +404,7 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
403
404
|
|
|
404
405
|
const cellStatusEvents: EvalStatusEvent[] = [];
|
|
405
406
|
const cellDisplayOutputs: EvalDisplayOutput[] = [];
|
|
407
|
+
const cellImageNotes: string[] = [];
|
|
406
408
|
let cellHasMarkdown = false;
|
|
407
409
|
for (const output of result.displayOutputs) {
|
|
408
410
|
if (output.type === "json") {
|
|
@@ -410,8 +412,26 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
410
412
|
cellDisplayOutputs.push(output);
|
|
411
413
|
}
|
|
412
414
|
if (output.type === "image") {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
+
const resized = await resizeImage({
|
|
416
|
+
type: "image",
|
|
417
|
+
data: output.data,
|
|
418
|
+
mimeType: output.mimeType,
|
|
419
|
+
});
|
|
420
|
+
const image: ImageContent = {
|
|
421
|
+
type: "image",
|
|
422
|
+
data: resized.data,
|
|
423
|
+
mimeType: resized.mimeType,
|
|
424
|
+
};
|
|
425
|
+
images.push(image);
|
|
426
|
+
cellDisplayOutputs.push({
|
|
427
|
+
type: "image",
|
|
428
|
+
data: image.data,
|
|
429
|
+
mimeType: image.mimeType,
|
|
430
|
+
});
|
|
431
|
+
const dimensionNote = formatDimensionNote(resized);
|
|
432
|
+
if (dimensionNote) {
|
|
433
|
+
cellImageNotes.push(`display image ${cellImageNotes.length + 1}: ${dimensionNote}`);
|
|
434
|
+
}
|
|
415
435
|
}
|
|
416
436
|
if (output.type === "status") {
|
|
417
437
|
statusEvents.push(output.event);
|
|
@@ -423,9 +443,14 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
423
443
|
}
|
|
424
444
|
|
|
425
445
|
const stdoutTrimmed = result.output.trim();
|
|
446
|
+
const imageText = cellImageNotes.join("\n");
|
|
426
447
|
const displayText = formatDisplayOutputsForText(cellDisplayOutputs);
|
|
448
|
+
const visibleDisplayText =
|
|
449
|
+
displayText && imageText ? `${displayText}\n\n${imageText}` : displayText || imageText;
|
|
427
450
|
const cellOutput =
|
|
428
|
-
stdoutTrimmed &&
|
|
451
|
+
stdoutTrimmed && visibleDisplayText
|
|
452
|
+
? `${stdoutTrimmed}\n\n${visibleDisplayText}`
|
|
453
|
+
: stdoutTrimmed || visibleDisplayText;
|
|
429
454
|
cellResult.output = cellOutput;
|
|
430
455
|
cellResult.exitCode = result.exitCode;
|
|
431
456
|
cellResult.durationMs = durationMs;
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Matched lines are prefixed with `*`; context lines are prefixed with a single
|
|
5
5
|
* space so line numbers align in column. In hashline mode the line uses the
|
|
6
|
-
* editable `LINE:content` shape under a
|
|
7
|
-
* the legacy `LINE|content` display-only shape. Line numbers are never padded.
|
|
6
|
+
* editable `LINE:content` shape under a snapshot-tag header; in plain mode it
|
|
7
|
+
* keeps the legacy `LINE|content` display-only shape. Line numbers are never padded.
|
|
8
8
|
*/
|
|
9
9
|
export function formatMatchLine(
|
|
10
10
|
lineNumber: number,
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -28,6 +28,7 @@ const INTERNAL_SCHEMES_WITH_SELECTORS: Record<string, true> = {
|
|
|
28
28
|
pr: true,
|
|
29
29
|
rule: true,
|
|
30
30
|
skill: true,
|
|
31
|
+
vault: true,
|
|
31
32
|
};
|
|
32
33
|
// Schemes whose resource URIs are server-defined and may legitimately end
|
|
33
34
|
// with selector-shaped tails (e.g. `:raw`, `:conflicts`, `:1-50`, `/:raw`).
|
|
@@ -45,6 +46,7 @@ const TOP_LEVEL_INTERNAL_URL_PREFIXES = [
|
|
|
45
46
|
"rule://",
|
|
46
47
|
"local://",
|
|
47
48
|
"mcp://",
|
|
49
|
+
"vault://",
|
|
48
50
|
] as const;
|
|
49
51
|
|
|
50
52
|
function normalizeUnicodeSpaces(str: string): string {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
2
|
+
import { resolveLocalUrlToPath, resolveVaultUrlToPath } from "../internal-urls";
|
|
3
3
|
import type { ToolSession } from ".";
|
|
4
4
|
import { normalizeLocalScheme, resolveToCwd } from "./path-utils";
|
|
5
5
|
import { ToolError } from "./tool-errors";
|
|
6
6
|
|
|
7
|
+
const VAULT_SCHEME_PREFIX = "vault:";
|
|
7
8
|
const LOCAL_SCHEME_PREFIX = "local:";
|
|
8
9
|
const PLAN_ALIAS_FILE = "PLAN.md";
|
|
9
10
|
const LOCAL_PLAN_ALIAS = "local://PLAN.md";
|
|
@@ -17,6 +18,10 @@ function resolveRawPath(session: ToolSession, targetPath: string): string {
|
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
if (normalized.startsWith(VAULT_SCHEME_PREFIX)) {
|
|
22
|
+
return resolveVaultUrlToPath(normalized);
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
return resolveToCwd(normalized, session.cwd);
|
|
21
26
|
}
|
|
22
27
|
|
package/src/tools/read.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { formatHashlineHeader, formatNumberedLine, formatNumberedLines } from "@oh-my-pi/hashline";
|
|
5
5
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
6
6
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
7
7
|
import { glob, type SummaryResult, summarizeCode } from "@oh-my-pi/pi-natives";
|
|
@@ -118,39 +118,50 @@ function prependLineNumbers(text: string, startNum: number): string {
|
|
|
118
118
|
|
|
119
119
|
interface HashlineHeaderContext {
|
|
120
120
|
header: string;
|
|
121
|
-
|
|
122
|
-
fullText
|
|
121
|
+
tag: string;
|
|
122
|
+
fullText?: string;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function
|
|
125
|
+
function recordFullHashlineContext(
|
|
126
|
+
session: ToolSession,
|
|
127
|
+
absolutePath: string | undefined,
|
|
128
|
+
displayPath: string,
|
|
129
|
+
fullText: string,
|
|
130
|
+
): HashlineHeaderContext | undefined {
|
|
131
|
+
if (!absolutePath || !path.isAbsolute(absolutePath)) return undefined;
|
|
126
132
|
const normalized = normalizeToLF(fullText);
|
|
127
|
-
const
|
|
133
|
+
const tag = getFileSnapshotStore(session).recordContiguous(absolutePath, 1, normalized.split("\n"), {
|
|
134
|
+
fullText: normalized,
|
|
135
|
+
});
|
|
128
136
|
return {
|
|
129
|
-
header: formatHashlineHeader(displayPath,
|
|
130
|
-
|
|
137
|
+
header: formatHashlineHeader(displayPath, tag),
|
|
138
|
+
tag,
|
|
131
139
|
fullText: normalized,
|
|
132
140
|
};
|
|
133
141
|
}
|
|
134
142
|
|
|
135
|
-
async function readHashlineHeaderContext(
|
|
143
|
+
async function readHashlineHeaderContext(
|
|
144
|
+
session: ToolSession,
|
|
145
|
+
absolutePath: string,
|
|
146
|
+
cwd: string,
|
|
147
|
+
): Promise<HashlineHeaderContext> {
|
|
136
148
|
const fullText = await Bun.file(absolutePath).text();
|
|
137
|
-
|
|
149
|
+
const context = recordFullHashlineContext(
|
|
150
|
+
session,
|
|
151
|
+
absolutePath,
|
|
152
|
+
formatPathRelativeToCwd(absolutePath, cwd),
|
|
153
|
+
fullText,
|
|
154
|
+
);
|
|
155
|
+
if (!context) throw new ToolError(`Cannot record hashline snapshot for non-absolute path: ${absolutePath}`);
|
|
156
|
+
return context;
|
|
138
157
|
}
|
|
139
158
|
|
|
140
|
-
function
|
|
141
|
-
return
|
|
159
|
+
function hashlineHeaderContext(displayPath: string, tag: string): HashlineHeaderContext {
|
|
160
|
+
return { header: formatHashlineHeader(displayPath, tag), tag };
|
|
142
161
|
}
|
|
143
162
|
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
absolutePath: string | undefined,
|
|
147
|
-
context: HashlineHeaderContext | undefined,
|
|
148
|
-
): void {
|
|
149
|
-
if (!context || !absolutePath || !path.isAbsolute(absolutePath)) return;
|
|
150
|
-
getFileSnapshotStore(session).recordContiguous(absolutePath, 1, context.fullText.split("\n"), {
|
|
151
|
-
fullText: context.fullText,
|
|
152
|
-
fileHash: context.fileHash,
|
|
153
|
-
});
|
|
163
|
+
function prependHashlineHeader(text: string, context: HashlineHeaderContext | undefined): string {
|
|
164
|
+
return context ? `${context.header}\n${text}` : text;
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
function formatTextWithMode(
|
|
@@ -841,9 +852,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
841
852
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
842
853
|
const hashContext =
|
|
843
854
|
shouldAddHashLines && options.sourcePath
|
|
844
|
-
?
|
|
855
|
+
? recordFullHashlineContext(
|
|
856
|
+
this.session,
|
|
857
|
+
options.sourcePath,
|
|
858
|
+
formatPathRelativeToCwd(options.sourcePath, this.session.cwd),
|
|
859
|
+
text,
|
|
860
|
+
)
|
|
845
861
|
: undefined;
|
|
846
|
-
recordHashlineSnapshot(this.session, options.sourcePath, hashContext);
|
|
847
862
|
let emittedHashlineHeader = false;
|
|
848
863
|
const formatText = (content: string, startNum: number): string => {
|
|
849
864
|
details.displayContent = { text: content, startLine: startNum };
|
|
@@ -934,9 +949,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
934
949
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
935
950
|
const hashContext =
|
|
936
951
|
shouldAddHashLines && options.sourcePath
|
|
937
|
-
?
|
|
952
|
+
? recordFullHashlineContext(
|
|
953
|
+
this.session,
|
|
954
|
+
options.sourcePath,
|
|
955
|
+
formatPathRelativeToCwd(options.sourcePath, this.session.cwd),
|
|
956
|
+
text,
|
|
957
|
+
)
|
|
938
958
|
: undefined;
|
|
939
|
-
recordHashlineSnapshot(this.session, options.sourcePath, hashContext);
|
|
940
959
|
let emittedHashlineHeader = false;
|
|
941
960
|
|
|
942
961
|
const resultBuilder = toolResult(details);
|
|
@@ -1014,11 +1033,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1014
1033
|
|
|
1015
1034
|
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1016
1035
|
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1017
|
-
const
|
|
1018
|
-
? await readHashlineHeaderContext(absolutePath, this.session.cwd)
|
|
1019
|
-
: undefined;
|
|
1020
|
-
recordHashlineSnapshot(this.session, absolutePath, hashContext);
|
|
1021
|
-
let emittedHashlineHeader = false;
|
|
1036
|
+
const sparseSnapshotEntries: Array<readonly [number, string]> = [];
|
|
1022
1037
|
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
1023
1038
|
|
|
1024
1039
|
const blocks: string[] = [];
|
|
@@ -1058,22 +1073,19 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1058
1073
|
}
|
|
1059
1074
|
}
|
|
1060
1075
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
absolutePath,
|
|
1064
|
-
range.startLine,
|
|
1065
|
-
collectedLines,
|
|
1066
|
-
hashContext ? { fullText: hashContext.fullText, fileHash: hashContext.fileHash } : {},
|
|
1067
|
-
);
|
|
1076
|
+
for (let index = 0; index < collectedLines.length; index++) {
|
|
1077
|
+
sparseSnapshotEntries.push([range.startLine + index, collectedLines[index]]);
|
|
1068
1078
|
}
|
|
1069
1079
|
|
|
1070
1080
|
const blockText = collectedLines.join("\n");
|
|
1071
|
-
|
|
1072
|
-
blocks.push(hashContext && !emittedHashlineHeader ? prependHashlineHeader(formatted, hashContext) : formatted);
|
|
1073
|
-
if (hashContext) emittedHashlineHeader = true;
|
|
1081
|
+
blocks.push(formatTextWithMode(blockText, range.startLine, shouldAddHashLines, shouldAddLineNumbers));
|
|
1074
1082
|
}
|
|
1075
1083
|
|
|
1076
1084
|
let outputText = blocks.join("\n\n…\n\n");
|
|
1085
|
+
if (shouldAddHashLines && sparseSnapshotEntries.length > 0 && outputText) {
|
|
1086
|
+
const tag = getFileSnapshotStore(this.session).recordSparse(absolutePath, sparseSnapshotEntries);
|
|
1087
|
+
outputText = `${formatHashlineHeader(formatPathRelativeToCwd(absolutePath, this.session.cwd), tag)}\n${outputText}`;
|
|
1088
|
+
}
|
|
1077
1089
|
if (notices.length > 0) {
|
|
1078
1090
|
outputText = outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n");
|
|
1079
1091
|
}
|
|
@@ -1726,9 +1738,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1726
1738
|
renderedSummary.elidedLines,
|
|
1727
1739
|
);
|
|
1728
1740
|
const summaryHashContext = displayMode.hashLines
|
|
1729
|
-
? await readHashlineHeaderContext(absolutePath, this.session.cwd)
|
|
1741
|
+
? await readHashlineHeaderContext(this.session, absolutePath, this.session.cwd)
|
|
1730
1742
|
: undefined;
|
|
1731
|
-
recordHashlineSnapshot(this.session, absolutePath, summaryHashContext);
|
|
1732
1743
|
const bodyText = footer ? `${renderedSummary.text}\n\n${footer}` : renderedSummary.text;
|
|
1733
1744
|
const modelText = prependHashlineHeader(bodyText, summaryHashContext);
|
|
1734
1745
|
details = {
|
|
@@ -1875,17 +1886,19 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1875
1886
|
|
|
1876
1887
|
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1877
1888
|
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
+
let hashContext: HashlineHeaderContext | undefined;
|
|
1890
|
+
if (shouldAddHashLines && collectedLines.length > 0 && !firstLineExceedsLimit) {
|
|
1891
|
+
const store = getFileSnapshotStore(this.session);
|
|
1892
|
+
const tag =
|
|
1893
|
+
offset === undefined && limit === undefined && !wasTruncated && columnTruncated === 0
|
|
1894
|
+
? (() => {
|
|
1895
|
+
const normalized = normalizeToLF(selectedContent);
|
|
1896
|
+
return store.recordContiguous(absolutePath, 1, normalized.split("\n"), {
|
|
1897
|
+
fullText: normalized,
|
|
1898
|
+
});
|
|
1899
|
+
})()
|
|
1900
|
+
: store.recordContiguous(absolutePath, startLineDisplay, collectedLines);
|
|
1901
|
+
hashContext = hashlineHeaderContext(formatPathRelativeToCwd(absolutePath, this.session.cwd), tag);
|
|
1889
1902
|
}
|
|
1890
1903
|
|
|
1891
1904
|
let capturedDisplayContent: { text: string; startLine: number } | undefined;
|
|
@@ -2031,9 +2044,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2031
2044
|
|
|
2032
2045
|
const rawText = region.lines.join("\n");
|
|
2033
2046
|
const hashContext = shouldAddHashLines
|
|
2034
|
-
?
|
|
2047
|
+
? hashlineHeaderContext(
|
|
2048
|
+
formatPathRelativeToCwd(entry.absolutePath, this.session.cwd),
|
|
2049
|
+
getFileSnapshotStore(this.session).recordContiguous(entry.absolutePath, region.startLine, region.lines),
|
|
2050
|
+
)
|
|
2035
2051
|
: undefined;
|
|
2036
|
-
recordHashlineSnapshot(this.session, entry.absolutePath, hashContext);
|
|
2037
2052
|
const formattedBody = formatTextWithMode(rawText, region.startLine, shouldAddHashLines, shouldAddLineNumbers);
|
|
2038
2053
|
const formattedText = prependHashlineHeader(formattedBody, hashContext);
|
|
2039
2054
|
|
|
@@ -469,7 +469,22 @@ export function truncateDiffByHunk(
|
|
|
469
469
|
diffText: string,
|
|
470
470
|
maxHunks: number,
|
|
471
471
|
maxLines: number,
|
|
472
|
+
options?: { fromTail?: boolean },
|
|
472
473
|
): { text: string; hiddenHunks: number; hiddenLines: number } {
|
|
474
|
+
if (options?.fromTail) {
|
|
475
|
+
// Streaming previews want to track the tail of the diff as new hunks
|
|
476
|
+
// arrive. Reversing the line buffer reuses the head-mode logic without
|
|
477
|
+
// duplicating the segment-budget bookkeeping: hunk runs survive
|
|
478
|
+
// reversal (a continuous `+`/`-` block stays contiguous) and so do the
|
|
479
|
+
// per-line `+`/`-` markers, so getDiffStats yields identical counts.
|
|
480
|
+
const reversed = (diffText ?? "").split("\n").reverse().join("\n");
|
|
481
|
+
const result = truncateDiffByHunk(reversed, maxHunks, maxLines);
|
|
482
|
+
return {
|
|
483
|
+
text: result.text.split("\n").reverse().join("\n"),
|
|
484
|
+
hiddenHunks: result.hiddenHunks,
|
|
485
|
+
hiddenLines: result.hiddenLines,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
473
488
|
const lines = diffText ? diffText.split("\n") : [];
|
|
474
489
|
const totalStats = getDiffStats(diffText);
|
|
475
490
|
|
package/src/tools/search.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
4
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
5
|
+
import { formatHashlineHeader } from "@oh-my-pi/hashline";
|
|
5
6
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
6
7
|
import { type GrepMatch, GrepOutputMode, type GrepResult, grep } from "@oh-my-pi/pi-natives";
|
|
7
8
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
@@ -609,16 +610,16 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
609
610
|
matchesByFile.get(relativePath)!.push(match);
|
|
610
611
|
}
|
|
611
612
|
const displayLines: string[] = [];
|
|
612
|
-
const hashContexts = new Map<string, { absolutePath: string;
|
|
613
|
+
const hashContexts = new Map<string, { absolutePath: string; tag?: string }>();
|
|
614
|
+
const snapshotStore = baseDisplayMode.hashLines ? getFileSnapshotStore(this.session) : undefined;
|
|
613
615
|
if (baseDisplayMode.hashLines) {
|
|
614
616
|
for (const relativePath of fileList) {
|
|
615
617
|
if (archiveDisplaySet.has(relativePath)) continue;
|
|
616
618
|
const absoluteFilePath = path.resolve(this.session.cwd, relativePath);
|
|
617
619
|
if (immutableSourcePaths.has(absoluteFilePath)) continue;
|
|
618
620
|
try {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
hashContexts.set(relativePath, { absolutePath: absoluteFilePath, fileHash });
|
|
621
|
+
await access(absoluteFilePath, constants.R_OK);
|
|
622
|
+
hashContexts.set(relativePath, { absolutePath: absoluteFilePath });
|
|
622
623
|
} catch {
|
|
623
624
|
// Best-effort: if the file disappeared between grep and render, fall back to plain line output.
|
|
624
625
|
}
|
|
@@ -671,9 +672,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
671
672
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
672
673
|
}
|
|
673
674
|
if (cacheEntries.length > 0 && hashContext) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
});
|
|
675
|
+
const tag = snapshotStore?.recordSparse(hashContext.absolutePath, cacheEntries);
|
|
676
|
+
if (tag) hashContext.tag = tag;
|
|
677
677
|
}
|
|
678
678
|
return { model: modelOut, display: displayOut };
|
|
679
679
|
};
|
|
@@ -684,7 +684,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
684
684
|
return {
|
|
685
685
|
modelLines: rendered.model,
|
|
686
686
|
displayLines: rendered.display,
|
|
687
|
-
headerSuffix: hashContext ? `#${hashContext.
|
|
687
|
+
headerSuffix: hashContext?.tag ? `#${hashContext.tag}` : "",
|
|
688
688
|
skip: rendered.model.length === 0,
|
|
689
689
|
};
|
|
690
690
|
});
|
|
@@ -699,8 +699,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
699
699
|
displayLines.push("");
|
|
700
700
|
}
|
|
701
701
|
const hashContext = hashContexts.get(relativePath);
|
|
702
|
-
if (hashContext) {
|
|
703
|
-
outputLines.push(formatHashlineHeader(relativePath, hashContext.
|
|
702
|
+
if (hashContext?.tag) {
|
|
703
|
+
outputLines.push(formatHashlineHeader(relativePath, hashContext.tag));
|
|
704
704
|
}
|
|
705
705
|
outputLines.push(...rendered.model);
|
|
706
706
|
displayLines.push(...rendered.display);
|
package/src/tools/write.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
|
|
4
5
|
import { stripHashlinePrefixes } from "@oh-my-pi/hashline";
|
|
5
6
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
6
7
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
7
8
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
8
9
|
import { isEnoent, isRecord, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
9
10
|
import * as z from "zod/v4";
|
|
11
|
+
|
|
10
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
13
|
import { InternalUrlRouter } from "../internal-urls";
|
|
12
14
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
@@ -53,6 +55,8 @@ import {
|
|
|
53
55
|
import { ToolError } from "./tool-errors";
|
|
54
56
|
import { toolResult } from "./tool-result";
|
|
55
57
|
|
|
58
|
+
const LOOSE_HASHLINE_HEADER_RE = /^\s*¶\S+#[^ \t\r\n]*\s*$/;
|
|
59
|
+
|
|
56
60
|
let fflateModulePromise: Promise<typeof import("fflate")> | undefined;
|
|
57
61
|
async function loadFflate(): Promise<typeof import("fflate")> {
|
|
58
62
|
if (!fflateModulePromise) fflateModulePromise = import("fflate");
|
|
@@ -70,6 +74,33 @@ export type WriteToolInput = z.infer<typeof writeSchema>;
|
|
|
70
74
|
export interface WriteToolDetails {
|
|
71
75
|
diagnostics?: FileDiagnosticsResult;
|
|
72
76
|
meta?: OutputMeta;
|
|
77
|
+
/** Set when the file was auto-chmod'd because content begins with a `#!` shebang. */
|
|
78
|
+
madeExecutable?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Strip hashline display prefixes from write content.
|
|
83
|
+
*
|
|
84
|
+
* Includes a fallback for loosely-formed section headers that still carry
|
|
85
|
+
* line-number prefixes (for example legacy or malformed hashline echoes).
|
|
86
|
+
*/
|
|
87
|
+
function stripWriteContentWithPotentialLooseHeader(lines: string[]): { text: string; stripped: boolean } {
|
|
88
|
+
const cleaned = stripHashlinePrefixes(lines);
|
|
89
|
+
if (cleaned !== lines) {
|
|
90
|
+
return { text: cleaned.join("\n"), stripped: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const headerIndex = lines.findIndex(line => line.trim().length > 0);
|
|
94
|
+
if (headerIndex === -1 || !LOOSE_HASHLINE_HEADER_RE.test(lines[headerIndex])) {
|
|
95
|
+
return { text: lines.join("\n"), stripped: false };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const linesWithoutHeader = lines.slice(0, headerIndex).concat(lines.slice(headerIndex + 1));
|
|
99
|
+
const cleanedWithoutHeader = stripHashlinePrefixes(linesWithoutHeader);
|
|
100
|
+
if (cleanedWithoutHeader === linesWithoutHeader) {
|
|
101
|
+
return { text: lines.join("\n"), stripped: false };
|
|
102
|
+
}
|
|
103
|
+
return { text: cleanedWithoutHeader.join("\n"), stripped: true };
|
|
73
104
|
}
|
|
74
105
|
|
|
75
106
|
/**
|
|
@@ -82,10 +113,7 @@ function stripWriteContent(session: ToolSession, content: string): { text: strin
|
|
|
82
113
|
if (!resolveFileDisplayMode(session).hashLines) {
|
|
83
114
|
return { text: content, stripped: false };
|
|
84
115
|
}
|
|
85
|
-
|
|
86
|
-
const cleaned = stripHashlinePrefixes(lines);
|
|
87
|
-
if (cleaned === lines) return { text: content, stripped: false };
|
|
88
|
-
return { text: cleaned.join("\n"), stripped: true };
|
|
116
|
+
return stripWriteContentWithPotentialLooseHeader(content.split("\n"));
|
|
89
117
|
}
|
|
90
118
|
|
|
91
119
|
/**
|
|
@@ -103,6 +131,28 @@ function appendNoteToResult(result: AgentToolResult<WriteToolDetails>, note: str
|
|
|
103
131
|
}
|
|
104
132
|
}
|
|
105
133
|
|
|
134
|
+
/**
|
|
135
|
+
* If `content` begins with a `#!` shebang, ensure the file is executable.
|
|
136
|
+
*
|
|
137
|
+
* Mirrors `chmod a+x` (adds user/group/other execute bits to existing mode).
|
|
138
|
+
* Errors are swallowed: chmod failure (e.g. Windows ACL, read-only mount)
|
|
139
|
+
* MUST NOT fail an otherwise successful write. Returns whether the mode
|
|
140
|
+
* actually changed so the caller can surface a note.
|
|
141
|
+
*/
|
|
142
|
+
async function maybeMarkExecutableForShebang(absolutePath: string, content: string): Promise<boolean> {
|
|
143
|
+
if (!content.startsWith("#!")) return false;
|
|
144
|
+
try {
|
|
145
|
+
const stat = await fs.stat(absolutePath);
|
|
146
|
+
const currentMode = stat.mode & 0o7777;
|
|
147
|
+
const newMode = currentMode | 0o111;
|
|
148
|
+
if (newMode === currentMode) return false;
|
|
149
|
+
await fs.chmod(absolutePath, newMode);
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
106
156
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
107
157
|
// Tool Class
|
|
108
158
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -772,6 +822,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
772
822
|
|
|
773
823
|
const diagnostics = await this.#writethrough(absolutePath, cleanContent, signal, undefined, batchRequest);
|
|
774
824
|
invalidateFsScanAfterWrite(absolutePath);
|
|
825
|
+
const madeExecutable = await maybeMarkExecutableForShebang(absolutePath, cleanContent);
|
|
775
826
|
|
|
776
827
|
const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
777
828
|
let resultText = `Successfully wrote ${cleanContent.length} bytes to ${displayPath}`;
|
|
@@ -781,7 +832,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
781
832
|
if (!diagnostics) {
|
|
782
833
|
return {
|
|
783
834
|
content: [{ type: "text", text: resultText }],
|
|
784
|
-
details: {},
|
|
835
|
+
details: { madeExecutable: madeExecutable || undefined },
|
|
785
836
|
};
|
|
786
837
|
}
|
|
787
838
|
|
|
@@ -789,6 +840,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
789
840
|
content: [{ type: "text", text: resultText }],
|
|
790
841
|
details: {
|
|
791
842
|
diagnostics,
|
|
843
|
+
madeExecutable: madeExecutable || undefined,
|
|
792
844
|
meta: outputMeta()
|
|
793
845
|
.diagnostics(diagnostics.summary, diagnostics.messages ?? [])
|
|
794
846
|
.get(),
|
|
@@ -915,13 +967,16 @@ export const writeToolRenderer = {
|
|
|
915
967
|
const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
|
|
916
968
|
const lineCount = countLines(fileContent);
|
|
917
969
|
const lineSuffix = formatLineCountSuffix(lineCount, uiTheme);
|
|
970
|
+
const execSuffix = result.details?.madeExecutable
|
|
971
|
+
? `${uiTheme.fg("dim", " · ")}${uiTheme.fg("success", "made executable!")}`
|
|
972
|
+
: "";
|
|
918
973
|
|
|
919
974
|
// Build header with status icon
|
|
920
975
|
const header = renderStatusLine(
|
|
921
976
|
{
|
|
922
977
|
icon: "success",
|
|
923
978
|
title: "Write",
|
|
924
|
-
description: `${langIcon} ${pathDisplay}${lineSuffix}`,
|
|
979
|
+
description: `${langIcon} ${pathDisplay}${lineSuffix}${execSuffix}`,
|
|
925
980
|
},
|
|
926
981
|
uiTheme,
|
|
927
982
|
);
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from "node:fs/promises";
|
|
9
9
|
import path from "node:path";
|
|
10
|
-
import {
|
|
10
|
+
import { formatHashlineHeader, formatNumberedLines, type SnapshotStore } from "@oh-my-pi/hashline";
|
|
11
11
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
12
12
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
13
13
|
import { glob } from "@oh-my-pi/pi-natives";
|
|
14
14
|
import { fuzzyMatch } from "@oh-my-pi/pi-tui";
|
|
15
15
|
import { formatAge, formatBytes, readImageMetadata } from "@oh-my-pi/pi-utils";
|
|
16
|
+
import { normalizeToLF } from "../edit/normalize";
|
|
16
17
|
import type { FileMentionMessage } from "../session/messages";
|
|
17
18
|
import {
|
|
18
19
|
DEFAULT_MAX_BYTES,
|
|
@@ -277,7 +278,7 @@ export function extractFileMentions(text: string): string[] {
|
|
|
277
278
|
export async function generateFileMentionMessages(
|
|
278
279
|
filePaths: string[],
|
|
279
280
|
cwd: string,
|
|
280
|
-
options?: { autoResizeImages?: boolean; useHashLines?: boolean },
|
|
281
|
+
options?: { autoResizeImages?: boolean; useHashLines?: boolean; snapshotStore?: SnapshotStore },
|
|
281
282
|
): Promise<AgentMessage[]> {
|
|
282
283
|
if (filePaths.length === 0) return [];
|
|
283
284
|
|
|
@@ -354,9 +355,14 @@ export async function generateFileMentionMessages(
|
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
const content = await Bun.file(absolutePath).text();
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
358
|
+
const snapshotStore = options?.useHashLines ? options.snapshotStore : undefined;
|
|
359
|
+
const normalized = snapshotStore ? normalizeToLF(content) : content;
|
|
360
|
+
let { output, lineCount } = buildTextOutput(normalized);
|
|
361
|
+
if (snapshotStore) {
|
|
362
|
+
const tag = snapshotStore.recordContiguous(absolutePath, 1, normalized.split("\n"), {
|
|
363
|
+
fullText: normalized,
|
|
364
|
+
});
|
|
365
|
+
output = `${formatHashlineHeader(resolvedPath, tag)}\n${formatNumberedLines(output)}`;
|
|
360
366
|
}
|
|
361
367
|
files.push({ path: resolvedPath, content: output, lineCount });
|
|
362
368
|
} catch {
|
|
@@ -19,8 +19,9 @@ import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
|
19
19
|
|
|
20
20
|
const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
21
21
|
const CODEX_RESPONSES_PATH = "/codex/responses";
|
|
22
|
-
const FALLBACK_MODEL = "gpt-5.
|
|
22
|
+
const FALLBACK_MODEL = "gpt-5.5";
|
|
23
23
|
const DEFAULT_MODEL_PREFERENCES = [
|
|
24
|
+
"gpt-5.5",
|
|
24
25
|
"gpt-5.4",
|
|
25
26
|
"gpt-5-codex",
|
|
26
27
|
"gpt-5",
|