@oh-my-pi/pi-coding-agent 14.8.0 → 14.9.0
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/package.json +16 -7
- package/src/config/model-resolver.ts +92 -35
- package/src/config/prompt-templates.ts +1 -1
- package/src/debug/index.ts +21 -0
- package/src/debug/raw-sse-buffer.ts +229 -0
- package/src/debug/raw-sse.ts +213 -0
- package/src/edit/index.ts +9 -10
- package/src/edit/streaming.ts +6 -5
- package/src/eval/js/context-manager.ts +91 -47
- package/src/extensibility/extensions/loader.ts +9 -3
- package/src/extensibility/extensions/types.ts +10 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +99 -20
- package/src/hashline/anchors.ts +113 -0
- package/src/hashline/apply.ts +732 -0
- package/src/hashline/bigrams.json +649 -0
- package/src/hashline/constants.ts +8 -0
- package/src/hashline/diff-preview.ts +43 -0
- package/src/hashline/diff.ts +56 -0
- package/src/hashline/execute.ts +268 -0
- package/src/{edit/modes/hashline.lark → hashline/grammar.lark} +1 -1
- package/src/{edit/line-hash.ts → hashline/hash.ts} +5 -651
- package/src/hashline/index.ts +14 -0
- package/src/hashline/input.ts +110 -0
- package/src/hashline/parser.ts +220 -0
- package/src/hashline/prefixes.ts +101 -0
- package/src/hashline/recovery.ts +72 -0
- package/src/hashline/stream.ts +123 -0
- package/src/hashline/types.ts +69 -0
- package/src/hashline/utils.ts +3 -0
- package/src/index.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/lsp/render.ts +4 -0
- package/src/memories/index.ts +13 -4
- package/src/modes/components/assistant-message.ts +55 -9
- package/src/modes/components/welcome.ts +114 -38
- package/src/modes/controllers/event-controller.ts +3 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +8 -1
- package/src/modes/interactive-mode.ts +50 -11
- package/src/modes/prompt-action-autocomplete.ts +3 -0
- package/src/modes/rpc/rpc-client.ts +53 -2
- package/src/modes/rpc/rpc-mode.ts +67 -1
- package/src/modes/rpc/rpc-types.ts +17 -2
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/ui-helpers.ts +3 -1
- package/src/prompts/agents/reviewer.md +14 -0
- package/src/prompts/tools/hashline.md +57 -10
- package/src/sdk.ts +4 -3
- package/src/session/agent-session.ts +195 -30
- package/src/session/compaction/branch-summarization.ts +4 -2
- package/src/session/compaction/compaction.ts +22 -3
- package/src/task/executor.ts +21 -2
- package/src/task/index.ts +4 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/match-line-format.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/title-generator.ts +11 -0
- package/src/edit/modes/hashline.ts +0 -2039
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
1
3
|
import * as path from "node:path";
|
|
4
|
+
import * as url from "node:url";
|
|
2
5
|
|
|
3
6
|
const LEGACY_PI_PACKAGE_MAP = {
|
|
4
7
|
"@mariozechner/pi-agent-core": "@oh-my-pi/pi-agent-core",
|
|
@@ -54,6 +57,10 @@ function getResolvedSpecifier(specifier: string): string {
|
|
|
54
57
|
return resolved;
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
function toImportSpecifier(resolvedPath: string): string {
|
|
61
|
+
return url.pathToFileURL(resolvedPath).href;
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
function rewriteLegacyPiImports(source: string): string {
|
|
58
65
|
return source.replace(
|
|
59
66
|
LEGACY_PI_IMPORT_SPECIFIER_REGEX,
|
|
@@ -63,42 +70,118 @@ function rewriteLegacyPiImports(source: string): string {
|
|
|
63
70
|
return match;
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
return `${prefix}${getResolvedSpecifier(remappedSpecifier)}${suffix}`;
|
|
73
|
+
return `${prefix}${toImportSpecifier(getResolvedSpecifier(remappedSpecifier))}${suffix}`;
|
|
67
74
|
},
|
|
68
75
|
);
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
// Match `from "..."
|
|
78
|
+
// Match static `from "..."` / `from '...'` import specifiers.
|
|
79
|
+
const STATIC_IMPORT_SPECIFIER_REGEX = /(from\s+["'])([^"']+)(["'])/g;
|
|
80
|
+
// Match static imports plus dynamic `import("...")` / `import('...')` specifiers.
|
|
72
81
|
const ANY_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s*\(\s*)["'])([^"']+)(["'])/g;
|
|
73
82
|
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
/** Resolve bare imports against the extension directory before loading mirrored legacy Pi files. */
|
|
84
|
+
function isUrlLikeSpecifier(specifier: string): boolean {
|
|
85
|
+
return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(specifier);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function shouldPreserveImportSpecifier(specifier: string): boolean {
|
|
89
|
+
return specifier.startsWith(".") || path.isAbsolute(specifier) || isUrlLikeSpecifier(specifier);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function toRewrittenImportSpecifier(resolvedPath: string): string {
|
|
93
|
+
return isUrlLikeSpecifier(resolvedPath) ? resolvedPath : toImportSpecifier(resolvedPath);
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
function rewriteBareImportsForLegacyExtension(source: string, importerPath: string): string {
|
|
82
97
|
const importerDir = path.dirname(importerPath);
|
|
83
98
|
return source.replace(ANY_IMPORT_SPECIFIER_REGEX, (match, prefix: string, specifier: string, suffix: string) => {
|
|
84
99
|
// Skip relative, absolute, URL-style, and already-resolved Node specifiers.
|
|
85
|
-
if (
|
|
86
|
-
specifier.startsWith(".") ||
|
|
87
|
-
specifier.startsWith("/") ||
|
|
88
|
-
specifier.startsWith("node:") ||
|
|
89
|
-
specifier.includes("://")
|
|
90
|
-
) {
|
|
100
|
+
if (shouldPreserveImportSpecifier(specifier)) {
|
|
91
101
|
return match;
|
|
92
102
|
}
|
|
93
103
|
try {
|
|
94
104
|
const resolved = Bun.resolveSync(specifier, importerDir);
|
|
95
|
-
return `${prefix}${resolved}${suffix}`;
|
|
105
|
+
return `${prefix}${toRewrittenImportSpecifier(resolved)}${suffix}`;
|
|
96
106
|
} catch {
|
|
97
107
|
return match;
|
|
98
108
|
}
|
|
99
109
|
});
|
|
100
110
|
}
|
|
101
111
|
|
|
112
|
+
interface LegacyPiMirrorState {
|
|
113
|
+
root: string;
|
|
114
|
+
seen: Map<string, string>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getMirrorPath(sourcePath: string, state: LegacyPiMirrorState): string {
|
|
118
|
+
const extension = path.extname(sourcePath) || ".js";
|
|
119
|
+
const digest = Bun.hash(sourcePath).toString(36);
|
|
120
|
+
return path.join(state.root, `${digest}${extension}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function rewriteRelativeImportsForLegacyExtension(
|
|
124
|
+
source: string,
|
|
125
|
+
importerPath: string,
|
|
126
|
+
state: LegacyPiMirrorState,
|
|
127
|
+
): Promise<string> {
|
|
128
|
+
const replacements = new Map<string, string>();
|
|
129
|
+
|
|
130
|
+
for (const match of source.matchAll(STATIC_IMPORT_SPECIFIER_REGEX)) {
|
|
131
|
+
const specifier = match[2];
|
|
132
|
+
if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const resolved = Bun.resolveSync(specifier, path.dirname(importerPath));
|
|
137
|
+
const mirrored = await mirrorLegacyPiFile(resolved, state);
|
|
138
|
+
replacements.set(specifier, toImportSpecifier(mirrored));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (replacements.size === 0) {
|
|
142
|
+
return source;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return source.replace(STATIC_IMPORT_SPECIFIER_REGEX, (match, prefix: string, specifier: string, suffix: string) => {
|
|
146
|
+
const replacement = replacements.get(specifier);
|
|
147
|
+
return replacement ? `${prefix}${replacement}${suffix}` : match;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function rewriteLegacyPiImportsForRuntime(
|
|
152
|
+
source: string,
|
|
153
|
+
importerPath: string,
|
|
154
|
+
state: LegacyPiMirrorState,
|
|
155
|
+
): Promise<string> {
|
|
156
|
+
const withRelativeResolved = await rewriteRelativeImportsForLegacyExtension(source, importerPath, state);
|
|
157
|
+
const withLegacyRemap = rewriteLegacyPiImports(withRelativeResolved);
|
|
158
|
+
return rewriteBareImportsForLegacyExtension(withLegacyRemap, importerPath);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function mirrorLegacyPiFile(sourcePath: string, state: LegacyPiMirrorState): Promise<string> {
|
|
162
|
+
const resolvedPath = path.resolve(sourcePath);
|
|
163
|
+
const cached = state.seen.get(resolvedPath);
|
|
164
|
+
if (cached) {
|
|
165
|
+
return cached;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const mirrorPath = getMirrorPath(resolvedPath, state);
|
|
169
|
+
state.seen.set(resolvedPath, mirrorPath);
|
|
170
|
+
|
|
171
|
+
const raw = await Bun.file(resolvedPath).text();
|
|
172
|
+
const rewritten = await rewriteLegacyPiImportsForRuntime(raw, resolvedPath, state);
|
|
173
|
+
await Bun.write(mirrorPath, rewritten);
|
|
174
|
+
return mirrorPath;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function loadLegacyPiModule(resolvedPath: string): Promise<unknown> {
|
|
178
|
+
const root = path.join(os.tmpdir(), "omp-legacy-pi-file", Bun.hash(resolvedPath).toString(36));
|
|
179
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
180
|
+
const state: LegacyPiMirrorState = { root, seen: new Map() };
|
|
181
|
+
const mirroredEntry = await mirrorLegacyPiFile(resolvedPath, state);
|
|
182
|
+
return import(`${toImportSpecifier(mirroredEntry)}?mtime=${Date.now()}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
102
185
|
function getLoader(path: string): "js" | "jsx" | "ts" | "tsx" {
|
|
103
186
|
if (path.endsWith(".tsx")) {
|
|
104
187
|
return "tsx";
|
|
@@ -150,10 +233,6 @@ export function installLegacyPiSpecifierShim(): void {
|
|
|
150
233
|
|
|
151
234
|
build.onLoad({ filter: /\.[cm]?[jt]sx?$/, namespace: LEGACY_PI_FILE_NAMESPACE }, async args => {
|
|
152
235
|
const raw = await Bun.file(args.path).text();
|
|
153
|
-
// Bare specifiers (e.g. "lodash", "@scope/pkg/sub") imported from a legacy-namespaced
|
|
154
|
-
// extension file would otherwise bypass Node-style node_modules lookup because the
|
|
155
|
-
// importer lives in a custom namespace. Pre-resolve them to absolute paths so the
|
|
156
|
-
// extension's own node_modules are honored.
|
|
157
236
|
const withLegacyRemap = rewriteLegacyPiImports(raw);
|
|
158
237
|
const withBareResolved = rewriteBareImportsForLegacyExtension(withLegacyRemap, args.path);
|
|
159
238
|
return {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { formatCodeFrameLine } from "../tools/render-utils";
|
|
2
|
+
import { MISMATCH_CONTEXT } from "./constants";
|
|
3
|
+
import { computeLineHash, describeAnchorExamples, HL_ANCHOR_RE_RAW, HL_BODY_SEP } from "./hash";
|
|
4
|
+
import type { HashMismatch } from "./types";
|
|
5
|
+
|
|
6
|
+
const HL_HASH_HINT_RE = /^[a-z]{2}$/i;
|
|
7
|
+
const HL_ANCHOR_EXAMPLES = describeAnchorExamples("160");
|
|
8
|
+
const PARSE_TAG_RE = new RegExp(`^${HL_ANCHOR_RE_RAW}`);
|
|
9
|
+
|
|
10
|
+
export function formatFullAnchorRequirement(raw?: string): string {
|
|
11
|
+
const suffix = typeof raw === "string" ? raw.trim() : "";
|
|
12
|
+
const hashOnlyHint = HL_HASH_HINT_RE.test(suffix)
|
|
13
|
+
? ` It looks like you supplied only the hash suffix (${JSON.stringify(suffix)}). ` +
|
|
14
|
+
`Copy the full anchor exactly as shown (for example, "160${suffix}").`
|
|
15
|
+
: "";
|
|
16
|
+
const received = raw === undefined ? "" : ` Received ${JSON.stringify(raw)}.`;
|
|
17
|
+
return (
|
|
18
|
+
`the full anchor exactly as shown by read/search output ` +
|
|
19
|
+
`(line number + hash, for example ${HL_ANCHOR_EXAMPLES})${received}${hashOnlyHint}`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseTag(ref: string): { line: number; hash: string } {
|
|
24
|
+
const match = ref.match(PARSE_TAG_RE);
|
|
25
|
+
if (!match) {
|
|
26
|
+
throw new Error(`Invalid line reference. Expected ${formatFullAnchorRequirement(ref)}.`);
|
|
27
|
+
}
|
|
28
|
+
const line = Number.parseInt(match[1], 10);
|
|
29
|
+
if (line < 1) throw new Error(`Line number must be >= 1, got ${line} in "${ref}".`);
|
|
30
|
+
return { line, hash: match[2] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getMismatchDisplayLines(mismatches: HashMismatch[], fileLines: string[]): number[] {
|
|
34
|
+
const displayLines = new Set<number>();
|
|
35
|
+
for (const mismatch of mismatches) {
|
|
36
|
+
const lo = Math.max(1, mismatch.line - MISMATCH_CONTEXT);
|
|
37
|
+
const hi = Math.min(fileLines.length, mismatch.line + MISMATCH_CONTEXT);
|
|
38
|
+
for (let lineNum = lo; lineNum <= hi; lineNum++) displayLines.add(lineNum);
|
|
39
|
+
}
|
|
40
|
+
return [...displayLines].sort((a, b) => a - b);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class HashlineMismatchError extends Error {
|
|
44
|
+
readonly remaps: ReadonlyMap<string, string>;
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
public readonly mismatches: HashMismatch[],
|
|
48
|
+
public readonly fileLines: string[],
|
|
49
|
+
) {
|
|
50
|
+
super(HashlineMismatchError.formatMessage(mismatches, fileLines));
|
|
51
|
+
this.name = "HashlineMismatchError";
|
|
52
|
+
|
|
53
|
+
const remaps = new Map<string, string>();
|
|
54
|
+
for (const mismatch of mismatches) {
|
|
55
|
+
const actual = computeLineHash(mismatch.line, fileLines[mismatch.line - 1] ?? "");
|
|
56
|
+
remaps.set(`${mismatch.line}${mismatch.expected}`, `${mismatch.line}${actual}`);
|
|
57
|
+
}
|
|
58
|
+
this.remaps = remaps;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get displayMessage(): string {
|
|
62
|
+
return HashlineMismatchError.formatDisplayMessage(this.mismatches, this.fileLines);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private static rejectionHeader(mismatches: HashMismatch[]): string[] {
|
|
66
|
+
const noun = mismatches.length > 1 ? "lines have" : "line has";
|
|
67
|
+
return [
|
|
68
|
+
`Edit rejected: ${mismatches.length} ${noun} changed since the last read (marked *).`,
|
|
69
|
+
"The edit was NOT applied, please use the updated file content shown below, and issue another edit tool-call.",
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static formatDisplayMessage(mismatches: HashMismatch[], fileLines: string[]): string {
|
|
74
|
+
const mismatchSet = new Set<number>(mismatches.map(m => m.line));
|
|
75
|
+
const displayLines = getMismatchDisplayLines(mismatches, fileLines);
|
|
76
|
+
const width = displayLines.reduce((cur, n) => Math.max(cur, String(n).length), 0);
|
|
77
|
+
|
|
78
|
+
const out = [...HashlineMismatchError.rejectionHeader(mismatches), ""];
|
|
79
|
+
let previous = -1;
|
|
80
|
+
for (const lineNum of displayLines) {
|
|
81
|
+
if (previous !== -1 && lineNum > previous + 1) out.push("...");
|
|
82
|
+
previous = lineNum;
|
|
83
|
+
const marker = mismatchSet.has(lineNum) ? "*" : " ";
|
|
84
|
+
out.push(formatCodeFrameLine(marker, lineNum, fileLines[lineNum - 1] ?? "", width));
|
|
85
|
+
}
|
|
86
|
+
return out.join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static formatMessage(mismatches: HashMismatch[], fileLines: string[]): string {
|
|
90
|
+
const mismatchSet = new Set<number>(mismatches.map(m => m.line));
|
|
91
|
+
const lines = HashlineMismatchError.rejectionHeader(mismatches);
|
|
92
|
+
let previous = -1;
|
|
93
|
+
for (const lineNum of getMismatchDisplayLines(mismatches, fileLines)) {
|
|
94
|
+
if (previous !== -1 && lineNum > previous + 1) lines.push("...");
|
|
95
|
+
previous = lineNum;
|
|
96
|
+
const text = fileLines[lineNum - 1] ?? "";
|
|
97
|
+
const hash = computeLineHash(lineNum, text);
|
|
98
|
+
const marker = mismatchSet.has(lineNum) ? "*" : " ";
|
|
99
|
+
lines.push(`${marker}${lineNum}${hash}${HL_BODY_SEP}${text}`);
|
|
100
|
+
}
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function validateLineRef(ref: { line: number; hash: string }, fileLines: string[]): void {
|
|
106
|
+
if (ref.line < 1 || ref.line > fileLines.length) {
|
|
107
|
+
throw new Error(`Line ${ref.line} does not exist (file has ${fileLines.length} lines)`);
|
|
108
|
+
}
|
|
109
|
+
const actualHash = computeLineHash(ref.line, fileLines[ref.line - 1] ?? "");
|
|
110
|
+
if (actualHash !== ref.hash) {
|
|
111
|
+
throw new HashlineMismatchError([{ line: ref.line, expected: ref.hash, actual: actualHash }], fileLines);
|
|
112
|
+
}
|
|
113
|
+
}
|