@oh-my-pi/pi-coding-agent 15.5.2 → 15.5.4
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 +38 -0
- package/dist/types/config/settings-schema.d.ts +27 -0
- package/dist/types/config.d.ts +31 -5
- package/dist/types/edit/file-snapshot-store.d.ts +18 -0
- package/dist/types/edit/hashline/diff.d.ts +30 -0
- package/dist/types/edit/hashline/execute.d.ts +29 -0
- package/dist/types/edit/hashline/filesystem.d.ts +57 -0
- package/dist/types/edit/hashline/index.d.ts +4 -0
- package/dist/types/edit/hashline/params.d.ts +12 -0
- package/dist/types/edit/index.d.ts +4 -3
- package/dist/types/edit/normalize.d.ts +4 -16
- package/dist/types/index.d.ts +0 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/tools/path-utils.d.ts +18 -0
- package/dist/types/utils/changelog.d.ts +8 -3
- package/package.json +8 -15
- package/src/config/settings-schema.ts +32 -0
- package/src/config.ts +42 -15
- package/src/edit/file-snapshot-store.ts +22 -0
- package/src/edit/hashline/diff.ts +88 -0
- package/src/edit/hashline/execute.ts +188 -0
- package/src/edit/hashline/filesystem.ts +129 -0
- package/src/edit/hashline/index.ts +4 -0
- package/src/edit/hashline/params.ts +11 -0
- package/src/edit/index.ts +7 -15
- package/src/edit/normalize.ts +11 -41
- package/src/edit/renderer.ts +1 -1
- package/src/edit/streaming.ts +8 -9
- package/src/index.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/sdk.ts +8 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/ast-grep.ts +3 -3
- package/src/tools/bash.ts +74 -10
- package/src/tools/index.ts +6 -5
- package/src/tools/path-utils.ts +81 -0
- package/src/tools/read.ts +14 -72
- package/src/tools/search.ts +136 -17
- package/src/tools/write.ts +3 -3
- package/src/utils/changelog.ts +11 -3
- package/src/utils/file-mentions.ts +1 -1
- package/dist/types/edit/file-read-cache.d.ts +0 -36
- package/dist/types/hashline/anchors.d.ts +0 -26
- package/dist/types/hashline/apply.d.ts +0 -14
- package/dist/types/hashline/constants.d.ts +0 -40
- package/dist/types/hashline/diff-preview.d.ts +0 -2
- package/dist/types/hashline/diff.d.ts +0 -16
- package/dist/types/hashline/execute.d.ts +0 -4
- package/dist/types/hashline/executor.d.ts +0 -56
- package/dist/types/hashline/hash.d.ts +0 -76
- package/dist/types/hashline/index.d.ts +0 -14
- package/dist/types/hashline/input.d.ts +0 -4
- package/dist/types/hashline/prefixes.d.ts +0 -7
- package/dist/types/hashline/recovery.d.ts +0 -21
- package/dist/types/hashline/stream.d.ts +0 -2
- package/dist/types/hashline/tokenizer.d.ts +0 -94
- package/dist/types/hashline/types.d.ts +0 -75
- package/src/edit/file-read-cache.ts +0 -138
- package/src/hashline/anchors.ts +0 -104
- package/src/hashline/apply.ts +0 -790
- package/src/hashline/bigrams.json +0 -649
- package/src/hashline/constants.ts +0 -51
- package/src/hashline/diff-preview.ts +0 -42
- package/src/hashline/diff.ts +0 -82
- package/src/hashline/execute.ts +0 -334
- package/src/hashline/executor.ts +0 -334
- package/src/hashline/grammar.lark +0 -23
- package/src/hashline/hash.ts +0 -131
- package/src/hashline/index.ts +0 -14
- package/src/hashline/input.ts +0 -137
- package/src/hashline/prefixes.ts +0 -111
- package/src/hashline/recovery.ts +0 -139
- package/src/hashline/stream.ts +0 -123
- package/src/hashline/tokenizer.ts +0 -473
- package/src/hashline/types.ts +0 -66
- package/src/prompts/tools/hashline.md +0 -63
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only hashline diff preview helpers used by the streaming edit
|
|
3
|
+
* renderer. Reads the target file, parses + applies the section's edits in
|
|
4
|
+
* memory (no FS write, no LSP writethrough), then hands the before/after
|
|
5
|
+
* pair to {@link generateDiffString} so the renderer can show the diff
|
|
6
|
+
* while the tool call is still streaming.
|
|
7
|
+
*
|
|
8
|
+
* Validation is intentionally light: only the section file hash is checked
|
|
9
|
+
* (so the preview goes red when anchors are stale), no plan-mode guards
|
|
10
|
+
* and no auto-generated-file refusal — those belong on the write path.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
applyEdits,
|
|
14
|
+
computeFileHash,
|
|
15
|
+
Patch as HashlinePatch,
|
|
16
|
+
normalizeToLF,
|
|
17
|
+
type Patch,
|
|
18
|
+
type PatchSection,
|
|
19
|
+
stripBom,
|
|
20
|
+
} from "@oh-my-pi/hashline";
|
|
21
|
+
import { resolveToCwd } from "../../tools/path-utils";
|
|
22
|
+
import { generateDiffString } from "../diff";
|
|
23
|
+
import { readEditFileText } from "../read-file";
|
|
24
|
+
|
|
25
|
+
export interface HashlineDiffOptions {
|
|
26
|
+
autoDropPureInsertDuplicates?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function readSectionText(absolutePath: string, sectionPath: string): Promise<string> {
|
|
30
|
+
try {
|
|
31
|
+
return await readEditFileText(absolutePath, sectionPath);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
throw new Error(message || `Unable to read ${sectionPath}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hasAnchorScoped(section: PatchSection): boolean {
|
|
39
|
+
return section.hasAnchorScopedEdit;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validateSectionHash(section: PatchSection, text: string): string | null {
|
|
43
|
+
if (section.fileHash === undefined) {
|
|
44
|
+
return hasAnchorScoped(section)
|
|
45
|
+
? `Missing hashline file hash for anchored edit to ${section.path}; use \`¶${section.path}#hash\` from your latest read.`
|
|
46
|
+
: null;
|
|
47
|
+
}
|
|
48
|
+
const currentHash = computeFileHash(text);
|
|
49
|
+
if (currentHash === section.fileHash) return null;
|
|
50
|
+
return `Hashline file hash mismatch for ${section.path}: section is bound to #${section.fileHash}, but current file hashes to #${currentHash}; re-read and try again.`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function computeHashlineSectionDiff(
|
|
54
|
+
section: PatchSection,
|
|
55
|
+
cwd: string,
|
|
56
|
+
options: HashlineDiffOptions = {},
|
|
57
|
+
): Promise<{ diff: string; firstChangedLine: number | undefined } | { error: string }> {
|
|
58
|
+
try {
|
|
59
|
+
const absolutePath = resolveToCwd(section.path, cwd);
|
|
60
|
+
const rawContent = await readSectionText(absolutePath, section.path);
|
|
61
|
+
const { text: content } = stripBom(rawContent);
|
|
62
|
+
const normalized = normalizeToLF(content);
|
|
63
|
+
const hashError = validateSectionHash(section, normalized);
|
|
64
|
+
if (hashError) return { error: hashError };
|
|
65
|
+
const result = applyEdits(normalized, [...section.edits], options);
|
|
66
|
+
if (normalized === result.text) return { error: `No changes would be made to ${section.path}.` };
|
|
67
|
+
return generateDiffString(normalized, result.text);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function computeHashlineDiff(
|
|
74
|
+
input: { input: string; path?: string },
|
|
75
|
+
cwd: string,
|
|
76
|
+
options: HashlineDiffOptions = {},
|
|
77
|
+
): Promise<{ diff: string; firstChangedLine: number | undefined } | { error: string }> {
|
|
78
|
+
let patch: Patch;
|
|
79
|
+
try {
|
|
80
|
+
patch = HashlinePatch.parse(input.input, { cwd, path: input.path });
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
83
|
+
}
|
|
84
|
+
if (patch.sections.length !== 1) {
|
|
85
|
+
return { error: "Streaming diff preview supports exactly one hashline section." };
|
|
86
|
+
}
|
|
87
|
+
return computeHashlineSectionDiff(patch.sections[0], cwd, options);
|
|
88
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coding-agent runner that drives the hashline {@link Patcher} on behalf of
|
|
3
|
+
* the `edit` tool. Converts a `{input, path?}` tool-call payload into a
|
|
4
|
+
* fully-applied patch, wraps the result in the agent's
|
|
5
|
+
* {@link AgentToolResult} shape, and attaches LSP diagnostics + `outputMeta`
|
|
6
|
+
* for the renderer.
|
|
7
|
+
*
|
|
8
|
+
* Multi-section patches are preflighted up front via {@link Patcher.prepare}
|
|
9
|
+
* so a partial batch never lands; the commit loop then narrows the LSP
|
|
10
|
+
* batch's `flush` flag to true only for the final write so diagnostics
|
|
11
|
+
* round-trip once.
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
buildCompactDiffPreview,
|
|
15
|
+
MismatchError as HashlineMismatchError,
|
|
16
|
+
Patch,
|
|
17
|
+
Patcher,
|
|
18
|
+
type PatchSectionResult,
|
|
19
|
+
type PreparedSection,
|
|
20
|
+
} from "@oh-my-pi/hashline";
|
|
21
|
+
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
22
|
+
import type { FileDiagnosticsResult, WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
|
|
23
|
+
import type { ToolSession } from "../../tools";
|
|
24
|
+
import { outputMeta } from "../../tools/output-meta";
|
|
25
|
+
import { generateDiffString } from "../diff";
|
|
26
|
+
import { getFileSnapshotStore } from "../file-snapshot-store";
|
|
27
|
+
import type { EditToolDetails, EditToolPerFileResult, LspBatchRequest } from "../renderer";
|
|
28
|
+
import { HashlineFilesystem } from "./filesystem";
|
|
29
|
+
import { type HashlineParams, hashlineEditParamsSchema } from "./params";
|
|
30
|
+
|
|
31
|
+
export interface ExecuteHashlineSingleOptions {
|
|
32
|
+
session: ToolSession;
|
|
33
|
+
input: string;
|
|
34
|
+
path?: string;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
batchRequest?: LspBatchRequest;
|
|
37
|
+
writethrough: WritethroughCallback;
|
|
38
|
+
beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getHashlineApplyOptions(session: ToolSession): { autoDropPureInsertDuplicates: boolean } {
|
|
42
|
+
return {
|
|
43
|
+
autoDropPureInsertDuplicates: session.settings.get("edit.hashlineAutoDropPureInsertDuplicates"),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function noChangeDiagnostic(path: string): string {
|
|
48
|
+
return `Edits to ${path} resulted in no changes being made.`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertUniqueCanonicalPaths(prepared: readonly PreparedSection[]): void {
|
|
52
|
+
const seen = new Map<string, string>();
|
|
53
|
+
for (const entry of prepared) {
|
|
54
|
+
const previous = seen.get(entry.canonicalPath);
|
|
55
|
+
if (previous !== undefined) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Multiple hashline sections resolve to the same file (${previous} and ${entry.section.path}). Merge their ops under one header before applying.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
seen.set(entry.canonicalPath, entry.section.path);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function narrowBatchRequest(outer: LspBatchRequest | undefined, isLast: boolean): LspBatchRequest | undefined {
|
|
65
|
+
if (!outer) return undefined;
|
|
66
|
+
return { id: outer.id, flush: isLast && outer.flush };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface RenderedSection {
|
|
70
|
+
toolResult: AgentToolResult<EditToolDetails, typeof hashlineEditParamsSchema>;
|
|
71
|
+
perFileResult: EditToolPerFileResult;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderSection(result: PatchSectionResult, diagnostics: FileDiagnosticsResult | undefined): RenderedSection {
|
|
75
|
+
if (result.op === "noop") {
|
|
76
|
+
const toolResult: AgentToolResult<EditToolDetails, typeof hashlineEditParamsSchema> = {
|
|
77
|
+
content: [{ type: "text", text: noChangeDiagnostic(result.path) }],
|
|
78
|
+
details: { diff: "", op: "update", meta: outputMeta().get() },
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
toolResult,
|
|
82
|
+
perFileResult: { path: result.path, diff: "", op: "update" },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const diff = generateDiffString(result.before, result.after);
|
|
87
|
+
const preview = buildCompactDiffPreview(diff.diff);
|
|
88
|
+
const meta = outputMeta()
|
|
89
|
+
.diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
|
|
90
|
+
.get();
|
|
91
|
+
|
|
92
|
+
const warningsBlock = result.warnings.length > 0 ? `\n\nWarnings:\n${result.warnings.join("\n")}` : "";
|
|
93
|
+
const previewBlock = preview.preview ? `\n${preview.preview}` : "";
|
|
94
|
+
const headline = preview.preview
|
|
95
|
+
? `${result.path}:`
|
|
96
|
+
: result.op === "create"
|
|
97
|
+
? `Created ${result.path}`
|
|
98
|
+
: `Updated ${result.path}`;
|
|
99
|
+
|
|
100
|
+
const firstChangedLine = result.firstChangedLine ?? diff.firstChangedLine;
|
|
101
|
+
return {
|
|
102
|
+
toolResult: {
|
|
103
|
+
content: [{ type: "text", text: `${headline}\n${result.header}${previewBlock}${warningsBlock}` }],
|
|
104
|
+
details: {
|
|
105
|
+
diff: diff.diff,
|
|
106
|
+
firstChangedLine,
|
|
107
|
+
diagnostics,
|
|
108
|
+
op: result.op,
|
|
109
|
+
meta,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
perFileResult: {
|
|
113
|
+
path: result.path,
|
|
114
|
+
diff: diff.diff,
|
|
115
|
+
firstChangedLine,
|
|
116
|
+
diagnostics,
|
|
117
|
+
op: result.op,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function executeHashlineSingle(
|
|
123
|
+
options: ExecuteHashlineSingleOptions,
|
|
124
|
+
): Promise<AgentToolResult<EditToolDetails, typeof hashlineEditParamsSchema>> {
|
|
125
|
+
const patch = Patch.parse(options.input, { cwd: options.session.cwd, path: options.path });
|
|
126
|
+
if (patch.sections.length === 0) {
|
|
127
|
+
throw new Error("No hashline sections found in input.");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const fs = new HashlineFilesystem({
|
|
131
|
+
session: options.session,
|
|
132
|
+
writethrough: options.writethrough,
|
|
133
|
+
beginDeferredDiagnosticsForPath: options.beginDeferredDiagnosticsForPath,
|
|
134
|
+
signal: options.signal,
|
|
135
|
+
batchRequest: options.batchRequest,
|
|
136
|
+
});
|
|
137
|
+
const snapshots = getFileSnapshotStore(options.session);
|
|
138
|
+
const applyOptions = getHashlineApplyOptions(options.session);
|
|
139
|
+
const patcher = new Patcher({ fs, snapshots, applyOptions });
|
|
140
|
+
|
|
141
|
+
// Single-section fast path: prepare, commit, render.
|
|
142
|
+
if (patch.sections.length === 1) {
|
|
143
|
+
fs.setBatchRequest(narrowBatchRequest(options.batchRequest, true));
|
|
144
|
+
const prepared = await patcher.prepare(patch.sections[0]);
|
|
145
|
+
const sectionResult = await patcher.commit(prepared);
|
|
146
|
+
if (sectionResult.op === "noop") {
|
|
147
|
+
return renderSection(sectionResult, undefined).toolResult;
|
|
148
|
+
}
|
|
149
|
+
return renderSection(sectionResult, fs.consumeDiagnostics(sectionResult.path)).toolResult;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Multi-section: prepare every section up front so we fail fast before
|
|
153
|
+
// any write hits the filesystem.
|
|
154
|
+
const prepared: PreparedSection[] = [];
|
|
155
|
+
for (const section of patch.sections) prepared.push(await patcher.prepare(section));
|
|
156
|
+
assertUniqueCanonicalPaths(prepared);
|
|
157
|
+
for (const entry of prepared) {
|
|
158
|
+
if (entry.isNoop) throw new Error(noChangeDiagnostic(entry.section.path));
|
|
159
|
+
}
|
|
160
|
+
// Then commit each one, narrowing the LSP batch flush flag to the final
|
|
161
|
+
// section only. A no-op apply mid-batch is treated as a hard failure —
|
|
162
|
+
// the model authored anchors that match the current file content.
|
|
163
|
+
const rendered: RenderedSection[] = [];
|
|
164
|
+
for (let i = 0; i < prepared.length; i++) {
|
|
165
|
+
const isLast = i === prepared.length - 1;
|
|
166
|
+
fs.setBatchRequest(narrowBatchRequest(options.batchRequest, isLast));
|
|
167
|
+
const sectionResult = await patcher.commit(prepared[i]);
|
|
168
|
+
if (sectionResult.op === "noop") throw new Error(noChangeDiagnostic(sectionResult.path));
|
|
169
|
+
rendered.push(renderSection(sectionResult, fs.consumeDiagnostics(sectionResult.path)));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: "text",
|
|
176
|
+
text: rendered
|
|
177
|
+
.map(r => r.toolResult.content.map(part => (part.type === "text" ? part.text : "")).join("\n"))
|
|
178
|
+
.join("\n\n"),
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
details: {
|
|
182
|
+
diff: rendered.map(r => r.toolResult.details?.diff ?? "").join("\n"),
|
|
183
|
+
perFileResults: rendered.map(r => r.perFileResult),
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export { HashlineMismatchError, type HashlineParams, hashlineEditParamsSchema };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coding-agent specific {@link Filesystem} adapter for the hashline patcher.
|
|
3
|
+
*
|
|
4
|
+
* Wires hashline's storage abstraction to the agent runtime:
|
|
5
|
+
*
|
|
6
|
+
* - Section paths are resolved through the plan-mode redirect so a bare
|
|
7
|
+
* `PLAN.md` lands at the canonical session artifact location.
|
|
8
|
+
* - Reads go through `readEditFileText` (notebook-aware) and the
|
|
9
|
+
* auto-generated-file guard.
|
|
10
|
+
* - Writes go through `serializeEditFileText` (notebook-aware) and the
|
|
11
|
+
* LSP writethrough, with FS-scan cache invalidation on success. The
|
|
12
|
+
* resulting `FileDiagnosticsResult` is captured per-path so the
|
|
13
|
+
* orchestrator can attach it to the tool result.
|
|
14
|
+
*
|
|
15
|
+
* Construct one per `executeHashlineSingle` call: per-section state
|
|
16
|
+
* (batch request, diagnostics) lives on the instance and isn't safe to
|
|
17
|
+
* share across concurrent edit tools.
|
|
18
|
+
*/
|
|
19
|
+
import { Filesystem, NotFoundError, type WriteResult } from "@oh-my-pi/hashline";
|
|
20
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
21
|
+
import type { FileDiagnosticsResult, WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
|
|
22
|
+
import type { ToolSession } from "../../tools";
|
|
23
|
+
import { assertEditableFileContent } from "../../tools/auto-generated-guard";
|
|
24
|
+
import { invalidateFsScanAfterWrite } from "../../tools/fs-cache-invalidation";
|
|
25
|
+
import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
|
|
26
|
+
import { readEditFileText, serializeEditFileText } from "../read-file";
|
|
27
|
+
import type { LspBatchRequest } from "../renderer";
|
|
28
|
+
|
|
29
|
+
export interface HashlineFilesystemOptions {
|
|
30
|
+
session: ToolSession;
|
|
31
|
+
writethrough: WritethroughCallback;
|
|
32
|
+
beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
/**
|
|
35
|
+
* Outer LSP batch request inherited from the tool-call context. The
|
|
36
|
+
* orchestrator narrows this per-section (flush only on the final write)
|
|
37
|
+
* via {@link HashlineFilesystem.setBatchRequest}.
|
|
38
|
+
*/
|
|
39
|
+
batchRequest?: LspBatchRequest;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class HashlineFilesystem extends Filesystem {
|
|
43
|
+
readonly session: ToolSession;
|
|
44
|
+
readonly #writethrough: WritethroughCallback;
|
|
45
|
+
readonly #beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
|
|
46
|
+
readonly #signal: AbortSignal | undefined;
|
|
47
|
+
#batchRequest: LspBatchRequest | undefined;
|
|
48
|
+
#diagnosticsByPath = new Map<string, FileDiagnosticsResult | undefined>();
|
|
49
|
+
|
|
50
|
+
constructor(options: HashlineFilesystemOptions) {
|
|
51
|
+
super();
|
|
52
|
+
this.session = options.session;
|
|
53
|
+
this.#writethrough = options.writethrough;
|
|
54
|
+
this.#beginDeferredDiagnosticsForPath = options.beginDeferredDiagnosticsForPath;
|
|
55
|
+
this.#signal = options.signal;
|
|
56
|
+
this.#batchRequest = options.batchRequest;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set the LSP batch request used for the next {@link writeText} call.
|
|
61
|
+
* Multi-section orchestrators flip the `flush` flag to true before the
|
|
62
|
+
* final section so LSP diagnostics flush in one round-trip.
|
|
63
|
+
*/
|
|
64
|
+
setBatchRequest(batchRequest: LspBatchRequest | undefined): void {
|
|
65
|
+
this.#batchRequest = batchRequest;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Look up (and clear) the diagnostics captured by the most-recent
|
|
70
|
+
* {@link writeText} call for `path`. Returns `undefined` if no write
|
|
71
|
+
* has happened or the writethrough returned no diagnostics.
|
|
72
|
+
*/
|
|
73
|
+
consumeDiagnostics(path: string): FileDiagnosticsResult | undefined {
|
|
74
|
+
const value = this.#diagnosticsByPath.get(path);
|
|
75
|
+
this.#diagnosticsByPath.delete(path);
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
resolveAbsolute(relativePath: string): string {
|
|
80
|
+
return resolvePlanPath(this.session, relativePath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
canonicalPath(relativePath: string): string {
|
|
84
|
+
return this.resolveAbsolute(relativePath);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async readText(relativePath: string): Promise<string> {
|
|
88
|
+
const absolutePath = this.resolveAbsolute(relativePath);
|
|
89
|
+
let content: string;
|
|
90
|
+
try {
|
|
91
|
+
content = await readEditFileText(absolutePath, relativePath);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (isEnoent(error)) throw new NotFoundError(relativePath, error);
|
|
94
|
+
if (error instanceof Error && error.message === `File not found: ${relativePath}`) {
|
|
95
|
+
throw new NotFoundError(relativePath, error);
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
// Refuse edits against generated files (lockfiles, models.json, …).
|
|
100
|
+
assertEditableFileContent(content, relativePath);
|
|
101
|
+
return content;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async preflightWrite(relativePath: string): Promise<void> {
|
|
105
|
+
enforcePlanModeWrite(this.session, relativePath, { op: "update" });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async writeText(relativePath: string, content: string): Promise<WriteResult> {
|
|
109
|
+
await this.preflightWrite(relativePath);
|
|
110
|
+
const absolutePath = this.resolveAbsolute(relativePath);
|
|
111
|
+
const finalContent = await serializeEditFileText(absolutePath, relativePath, content);
|
|
112
|
+
const diagnostics = await this.#writethrough(
|
|
113
|
+
absolutePath,
|
|
114
|
+
finalContent,
|
|
115
|
+
this.#signal,
|
|
116
|
+
Bun.file(absolutePath),
|
|
117
|
+
this.#batchRequest,
|
|
118
|
+
dst => (dst === absolutePath ? this.#beginDeferredDiagnosticsForPath(absolutePath) : undefined),
|
|
119
|
+
);
|
|
120
|
+
invalidateFsScanAfterWrite(absolutePath);
|
|
121
|
+
this.#diagnosticsByPath.set(relativePath, diagnostics);
|
|
122
|
+
return { text: finalContent };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async exists(relativePath: string): Promise<boolean> {
|
|
126
|
+
const absolutePath = this.resolveAbsolute(relativePath);
|
|
127
|
+
return Bun.file(absolutePath).exists();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schema for the `edit` tool's hashline mode payload. The schema is
|
|
3
|
+
* deliberately permissive (`.passthrough()`) so providers can attach extra
|
|
4
|
+
* keys without rejection; only `input` is required and `path` is an
|
|
5
|
+
* optional fallback used when the input lacks a `¶PATH#HASH` header.
|
|
6
|
+
*/
|
|
7
|
+
import * as z from "zod/v4";
|
|
8
|
+
|
|
9
|
+
export const hashlineEditParamsSchema = z.object({ input: z.string(), path: z.string().optional() }).passthrough();
|
|
10
|
+
|
|
11
|
+
export type HashlineParams = z.infer<typeof hashlineEditParamsSchema>;
|
package/src/edit/index.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
+
import { MismatchError as HashlineMismatchError } from "@oh-my-pi/hashline";
|
|
2
|
+
import hashlineGrammar from "@oh-my-pi/hashline/grammar.lark" with { type: "text" };
|
|
3
|
+
import hashlineDescription from "@oh-my-pi/hashline/prompt.md" with { type: "text" };
|
|
1
4
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
5
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
3
|
-
import {
|
|
4
|
-
executeHashlineSingle,
|
|
5
|
-
HashlineMismatchError,
|
|
6
|
-
type HashlineParams,
|
|
7
|
-
hashlineEditParamsSchema,
|
|
8
|
-
} from "../hashline";
|
|
9
|
-
import hashlineGrammarTemplate from "../hashline/grammar.lark" with { type: "text" };
|
|
10
|
-
import { resolveHashlineGrammarPlaceholders } from "../hashline/hash";
|
|
11
6
|
import {
|
|
12
7
|
createLspWritethrough,
|
|
13
8
|
type FileDiagnosticsResult,
|
|
@@ -16,28 +11,25 @@ import {
|
|
|
16
11
|
writethroughNoop,
|
|
17
12
|
} from "../lsp";
|
|
18
13
|
import applyPatchDescription from "../prompts/tools/apply-patch.md" with { type: "text" };
|
|
19
|
-
import hashlineDescription from "../prompts/tools/hashline.md" with { type: "text" };
|
|
20
14
|
import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
|
|
21
15
|
import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
|
|
22
16
|
import type { ToolSession } from "../tools";
|
|
23
17
|
import { truncateForPrompt } from "../tools/approval";
|
|
24
18
|
import { isInternalUrlPath } from "../tools/path-utils";
|
|
25
19
|
import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit-mode";
|
|
20
|
+
import { executeHashlineSingle, type HashlineParams, hashlineEditParamsSchema } from "./hashline";
|
|
26
21
|
import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
|
|
27
22
|
import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
|
|
28
23
|
import { executePatchSingle, type PatchEditEntry, type PatchParams, patchEditSchema } from "./modes/patch";
|
|
29
24
|
import { executeReplaceSingle, type ReplaceEditEntry, type ReplaceParams, replaceEditSchema } from "./modes/replace";
|
|
30
25
|
import { type EditToolDetails, type EditToolPerFileResult, getLspBatchRequest, type LspBatchRequest } from "./renderer";
|
|
31
26
|
|
|
27
|
+
export * from "@oh-my-pi/hashline";
|
|
32
28
|
export { DEFAULT_EDIT_MODE, type EditMode, normalizeEditMode } from "../utils/edit-mode";
|
|
33
29
|
export * from "./apply-patch";
|
|
34
30
|
export * from "./diff";
|
|
35
|
-
export * from "./file-
|
|
36
|
-
|
|
37
|
-
// Resolve hashline grammar placeholders from the TypeScript constants.
|
|
38
|
-
const hashlineGrammar = resolveHashlineGrammarPlaceholders(hashlineGrammarTemplate);
|
|
39
|
-
|
|
40
|
-
export * from "../hashline";
|
|
31
|
+
export * from "./file-snapshot-store";
|
|
32
|
+
export * from "./hashline";
|
|
41
33
|
export * from "./modes/apply-patch";
|
|
42
34
|
export * from "./modes/patch";
|
|
43
35
|
export * from "./modes/replace";
|
package/src/edit/normalize.ts
CHANGED
|
@@ -1,51 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Text normalization utilities for the edit tool.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Whitespace, Unicode, and indentation helpers. Line-ending and BOM
|
|
5
|
+
* primitives live in `@oh-my-pi/hashline` and are re-exported here so
|
|
6
|
+
* existing consumers see one stable surface.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import { padding } from "@oh-my-pi/pi-tui";
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const crlfIdx = content.indexOf("\r\n");
|
|
18
|
-
const lfIdx = content.indexOf("\n");
|
|
19
|
-
if (lfIdx === -1) return "\n";
|
|
20
|
-
if (crlfIdx === -1) return "\n";
|
|
21
|
-
return crlfIdx < lfIdx ? "\r\n" : "\n";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Normalize all line endings to LF */
|
|
25
|
-
export function normalizeToLF(text: string): string {
|
|
26
|
-
return text.replace(/\r\n?/g, "\n");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Restore line endings to the specified type */
|
|
30
|
-
export function restoreLineEndings(text: string, ending: LineEnding): string {
|
|
31
|
-
return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
35
|
-
// BOM Handling
|
|
36
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
37
|
-
|
|
38
|
-
export interface BomResult {
|
|
39
|
-
/** The BOM character if present, empty string otherwise */
|
|
40
|
-
bom: string;
|
|
41
|
-
/** The text without the BOM */
|
|
42
|
-
text: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Strip UTF-8 BOM if present */
|
|
46
|
-
export function stripBom(content: string): BomResult {
|
|
47
|
-
return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
|
|
48
|
-
}
|
|
11
|
+
export {
|
|
12
|
+
type BomResult,
|
|
13
|
+
detectLineEnding,
|
|
14
|
+
type LineEnding,
|
|
15
|
+
normalizeToLF,
|
|
16
|
+
restoreLineEndings,
|
|
17
|
+
stripBom,
|
|
18
|
+
} from "@oh-my-pi/hashline";
|
|
49
19
|
|
|
50
20
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
51
21
|
// Whitespace Utilities
|
package/src/edit/renderer.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Edit tool renderer and LSP batching helpers.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { HL_FILE_PREFIX } from "@oh-my-pi/hashline";
|
|
5
6
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
7
|
import { Text, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
|
|
7
8
|
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
8
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
9
|
-
import { HL_FILE_PREFIX } from "../hashline/hash";
|
|
10
10
|
import type { FileDiagnosticsResult } from "../lsp";
|
|
11
11
|
import { renderDiff as renderDiffColored } from "../modes/components/diff";
|
|
12
12
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
package/src/edit/streaming.ts
CHANGED
|
@@ -13,22 +13,21 @@
|
|
|
13
13
|
* the injected `editMode` rather than probing argument shape.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
17
16
|
import {
|
|
18
17
|
ABORT_MARKER,
|
|
19
18
|
BEGIN_PATCH_MARKER,
|
|
20
|
-
computeHashlineDiff,
|
|
21
|
-
computeHashlineSectionDiff,
|
|
22
19
|
containsRecognizableHashlineOperations,
|
|
23
20
|
END_PATCH_MARKER,
|
|
24
|
-
type HashlineInputSection,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} from "
|
|
21
|
+
type PatchSection as HashlineInputSection,
|
|
22
|
+
Patch as HashlinePatch,
|
|
23
|
+
Tokenizer as HashlineTokenizer,
|
|
24
|
+
} from "@oh-my-pi/hashline";
|
|
25
|
+
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
28
26
|
import type { Theme } from "../modes/theme/theme";
|
|
29
27
|
import { replaceTabs, truncateToWidth } from "../tools/render-utils";
|
|
30
28
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
31
29
|
import { computeEditDiff, type DiffError, type DiffResult } from "./diff";
|
|
30
|
+
import { computeHashlineDiff, computeHashlineSectionDiff } from "./hashline/diff";
|
|
32
31
|
import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
|
|
33
32
|
import { computePatchDiff, type PatchEditEntry } from "./modes/patch";
|
|
34
33
|
import type { ReplaceEditEntry } from "./modes/replace";
|
|
@@ -438,9 +437,9 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
|
|
|
438
437
|
}
|
|
439
438
|
ctx.signal.throwIfAborted();
|
|
440
439
|
|
|
441
|
-
let sections: HashlineInputSection[];
|
|
440
|
+
let sections: readonly HashlineInputSection[];
|
|
442
441
|
try {
|
|
443
|
-
sections =
|
|
442
|
+
sections = HashlinePatch.parse(input, { cwd: ctx.cwd, path: args.path }).sections;
|
|
444
443
|
} catch {
|
|
445
444
|
// Single-section fallback keeps the original error rendering for the
|
|
446
445
|
// "haven't typed `¶ PATH` yet" case.
|
package/src/index.ts
CHANGED
|
@@ -26,7 +26,6 @@ export * from "./extensibility/extensions";
|
|
|
26
26
|
export * from "./extensibility/skills";
|
|
27
27
|
// Slash commands
|
|
28
28
|
export { type FileSlashCommand, loadSlashCommands as discoverSlashCommands } from "./extensibility/slash-commands";
|
|
29
|
-
export * from "./hashline";
|
|
30
29
|
export type * from "./lsp";
|
|
31
30
|
// Main entry point
|
|
32
31
|
export * from "./main";
|