pi-hashline-edit-pro 7.0.0 → 7.1.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/README.md +28 -9
- package/index.ts +31 -28
- package/package.json +1 -1
- package/src/auto-read-all.ts +2 -2
- package/src/config-ui.ts +6 -1
- package/src/config.ts +6 -1
- package/src/edit-common.ts +15 -7
- package/src/model-gate.ts +1 -0
- package/src/outline/parsers.ts +0 -1
- package/src/outline/render.ts +154 -82
- package/src/outline/vendor/README.md +4 -0
- package/src/outline/vendor/parsers/blade.ts +1 -1
- package/src/outline/vendor/parsers/java.ts +144 -331
- package/src/outline/vendor/parsers/manager.ts +12 -183
- package/src/outline/vendor/parsers/php.ts +16 -81
- package/src/outline/vendor/parsers/sass.ts +0 -11
- package/src/outline/vendor/parsers/scss.ts +0 -8
- package/src/outline/vendor/types.ts +0 -1
- package/src/outline-tool.ts +184 -0
- package/src/prompts.ts +1 -0
- package/src/read.ts +1 -0
- package/src/structured.ts +32 -0
- package/tool-prompts/outline-guidelines.md +2 -0
- package/tool-prompts/outline-snippet.md +1 -0
- package/tool-prompts/outline.md +3 -0
- package/tool-prompts/read-guidelines.md +1 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
|
+
import { buildFileOutline } from "./outline";
|
|
5
|
+
import { MAX_HASH_LINES } from "./hashline";
|
|
6
|
+
import { loadFileKindAndText } from "./file-kind";
|
|
7
|
+
import { readNormFile, safeSnapId } from "./file-reader";
|
|
8
|
+
import { serveRows } from "./served";
|
|
9
|
+
import { toCwd, toDisplayPath } from "./paths";
|
|
10
|
+
import { formatAnchorReclaimNotice, takeReclaimedPaths, withAnchorSession } from "./anchor-registry";
|
|
11
|
+
import { valAccess, valKind } from "./validation";
|
|
12
|
+
import { abortIf, isRec, makePrepareArguments, rejectUnknownFields, splitLines } from "./utils";
|
|
13
|
+
import { loadP, loadGuidelines } from "./prompts";
|
|
14
|
+
import { DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
|
|
15
|
+
import { anchoredLine, outlineResultSchema, withStructuredErrors, type OutlineStructured } from "./structured";
|
|
16
|
+
|
|
17
|
+
const OUTLINE_DEFAULT_DEPTH = 2;
|
|
18
|
+
export const OUTLINE_MAX_DEPTH = 10;
|
|
19
|
+
|
|
20
|
+
export interface OutlineDetails {
|
|
21
|
+
truncated?: boolean;
|
|
22
|
+
complete?: boolean;
|
|
23
|
+
snapshotId?: string;
|
|
24
|
+
metrics?: { rows: number; truncated: boolean };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildOutlineToolSchema(codemode: boolean) {
|
|
28
|
+
return Type.Object({
|
|
29
|
+
path: Type.String({
|
|
30
|
+
description: "Path to the file to outline (relative or absolute).",
|
|
31
|
+
}),
|
|
32
|
+
depth: Type.Optional(
|
|
33
|
+
Type.Integer({
|
|
34
|
+
minimum: 0,
|
|
35
|
+
maximum: OUTLINE_MAX_DEPTH,
|
|
36
|
+
description: `Nesting depth to show: 0 lists top-level symbols only, 1 adds their members (default 2, maximum ${OUTLINE_MAX_DEPTH}).`,
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
...(codemode
|
|
40
|
+
? {
|
|
41
|
+
anchors: Type.Optional(
|
|
42
|
+
Type.Boolean({
|
|
43
|
+
description: "Return { text, lines, notices } with editable anchors instead of the plain outline text to a codemode script.",
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
: {}),
|
|
48
|
+
}, { additionalProperties: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function outlineDepth(value: unknown): number {
|
|
52
|
+
if (value === undefined) return OUTLINE_DEFAULT_DEPTH;
|
|
53
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > OUTLINE_MAX_DEPTH) {
|
|
54
|
+
throw new Error(`[E_BAD_SHAPE] Outline request field "depth" must be an integer from 0 to ${OUTLINE_MAX_DEPTH}.`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface OutlineReq {
|
|
60
|
+
path: string;
|
|
61
|
+
depth: number;
|
|
62
|
+
anchors: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const OUTLINE_KEYS = new Set(["path", "depth", "anchors"]);
|
|
66
|
+
|
|
67
|
+
export function normOutlineReq(request: unknown, allowAnchors = false): OutlineReq {
|
|
68
|
+
if (!isRec(request)) {
|
|
69
|
+
throw new Error("[E_BAD_SHAPE] Outline request must be an object.");
|
|
70
|
+
}
|
|
71
|
+
rejectUnknownFields(request, OUTLINE_KEYS, "Outline request");
|
|
72
|
+
if (typeof request.path !== "string" || request.path.length === 0) {
|
|
73
|
+
throw new Error('[E_BAD_SHAPE] Outline request requires a non-empty "path" string.');
|
|
74
|
+
}
|
|
75
|
+
if (request.anchors !== undefined) {
|
|
76
|
+
if (typeof request.anchors !== "boolean") {
|
|
77
|
+
throw new Error('[E_BAD_SHAPE] Outline request field "anchors" must be a boolean when provided.');
|
|
78
|
+
}
|
|
79
|
+
if (!allowAnchors) {
|
|
80
|
+
throw new Error('[E_BAD_SHAPE] Outline request field "anchors" is only valid in a codemode session.');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { path: request.path, depth: outlineDepth(request.depth), anchors: request.anchors === true };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function regOutline(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FLAGS): void {
|
|
87
|
+
const prompted = {
|
|
88
|
+
description: loadP("../tool-prompts/outline.md"),
|
|
89
|
+
snippet: loadP("../tool-prompts/outline-snippet.md"),
|
|
90
|
+
guidelines: loadGuidelines("../tool-prompts/outline-guidelines.md").map((guideline) => guideline.text),
|
|
91
|
+
};
|
|
92
|
+
if (flags.codemode) prompted.guidelines.push("`outline`: a codemode script receives the served outline text; pass `anchors: true` to receive `{ text, lines, notices }` with editable anchors.");
|
|
93
|
+
pi.registerTool({
|
|
94
|
+
name: "outline",
|
|
95
|
+
defaultActive: false,
|
|
96
|
+
label: "Outline",
|
|
97
|
+
description: prompted.description,
|
|
98
|
+
promptSnippet: prompted.snippet,
|
|
99
|
+
promptGuidelines: prompted.guidelines,
|
|
100
|
+
prepareArguments: makePrepareArguments(),
|
|
101
|
+
parameters: buildOutlineToolSchema(flags.codemode),
|
|
102
|
+
outputSchema: outlineResultSchema,
|
|
103
|
+
executionMode: "sequential",
|
|
104
|
+
renderCall(args: any, theme: any, context: any) {
|
|
105
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
106
|
+
const path = typeof args?.path === "string" && args.path.length > 0 ? args.path : "...";
|
|
107
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("outline"))} ${theme.fg("accent", path)}`);
|
|
108
|
+
return text;
|
|
109
|
+
},
|
|
110
|
+
renderResult(result, { isPartial, expanded }, theme, context) {
|
|
111
|
+
if (isPartial) return new Text(theme.fg("warning", "Outlining..."), 0, 0);
|
|
112
|
+
const raw = (result.content?.[0] as { text?: string } | undefined)?.text;
|
|
113
|
+
if (typeof raw !== "string") return new Text("", 0, 0);
|
|
114
|
+
if (context.isError) return new Text(theme.fg("error", raw), 0, 0);
|
|
115
|
+
if (expanded !== true && context.expanded !== true) return new Text("", 0, 0);
|
|
116
|
+
return new Text(raw, 0, 0);
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
120
|
+
return withStructuredErrors(signal, {}, () => withAnchorSession(ctx, async () => {
|
|
121
|
+
const request = normOutlineReq(params, flags.codemode);
|
|
122
|
+
const rawPath = request.path;
|
|
123
|
+
const depth = request.depth;
|
|
124
|
+
const absolutePath = toCwd(rawPath, ctx.cwd);
|
|
125
|
+
|
|
126
|
+
abortIf(signal);
|
|
127
|
+
await valAccess(absolutePath, rawPath);
|
|
128
|
+
|
|
129
|
+
abortIf(signal);
|
|
130
|
+
const file = await loadFileKindAndText(absolutePath, { maxLines: MAX_HASH_LINES, displayPath: rawPath });
|
|
131
|
+
valKind(file, rawPath);
|
|
132
|
+
const { normalized, fileHashes, absolutePath: resolvedPath, hadUtf8DecodeErrors } = await readNormFile(
|
|
133
|
+
rawPath,
|
|
134
|
+
ctx.cwd,
|
|
135
|
+
{ signal, preloadedFile: file, maxLines: MAX_HASH_LINES },
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
abortIf(signal);
|
|
139
|
+
const displayPath = toDisplayPath(ctx.cwd, resolvedPath, rawPath);
|
|
140
|
+
const fileLines = splitLines(normalized);
|
|
141
|
+
const section = await buildFileOutline({
|
|
142
|
+
displayPath,
|
|
143
|
+
content: normalized,
|
|
144
|
+
hashes: fileHashes,
|
|
145
|
+
maxDepth: depth,
|
|
146
|
+
});
|
|
147
|
+
serveRows(resolvedPath, fileHashes, fileLines, section.servedHashes);
|
|
148
|
+
|
|
149
|
+
const snapshotId = await safeSnapId(absolutePath, "outline");
|
|
150
|
+
const reclaimNotice = formatAnchorReclaimNotice(takeReclaimedPaths());
|
|
151
|
+
const notices = [
|
|
152
|
+
hadUtf8DecodeErrors ? "[Non-UTF-8 bytes shown as U+FFFD; editing rewrites the file as UTF-8.]" : undefined,
|
|
153
|
+
reclaimNotice,
|
|
154
|
+
].filter((part): part is string => part !== undefined);
|
|
155
|
+
const text = [section.text, ...notices].join("\n\n");
|
|
156
|
+
const served = new Set(section.servedHashes);
|
|
157
|
+
const structuredContent: OutlineStructured = {
|
|
158
|
+
ok: true,
|
|
159
|
+
kind: "outline",
|
|
160
|
+
path: rawPath,
|
|
161
|
+
text: section.text,
|
|
162
|
+
totalLines: normalized.length === 0 ? 0 : fileLines.length,
|
|
163
|
+
depth,
|
|
164
|
+
lines: section.rows.filter((row) => served.has(row.anchor)).map((row) => anchoredLine(row.line, row.text, row.anchor)),
|
|
165
|
+
truncated: section.truncated,
|
|
166
|
+
complete: section.complete,
|
|
167
|
+
notices,
|
|
168
|
+
};
|
|
169
|
+
const anchorsRequested = request.anchors;
|
|
170
|
+
const details: OutlineDetails = {
|
|
171
|
+
truncated: section.truncated,
|
|
172
|
+
complete: section.complete,
|
|
173
|
+
snapshotId,
|
|
174
|
+
metrics: { rows: section.rows.length, truncated: section.truncated },
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: "text", text }],
|
|
178
|
+
details,
|
|
179
|
+
structuredContent: flags.codemode && !anchorsRequested ? text : structuredContent,
|
|
180
|
+
};
|
|
181
|
+
}));
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
package/src/prompts.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface Guideline {
|
|
|
8
8
|
|
|
9
9
|
const GUIDELINE_IDS_BY_NAME: Record<string, readonly string[]> = {
|
|
10
10
|
"read-guidelines.md": ["read-reread", "read-outline", "read-match-served"],
|
|
11
|
+
"outline-guidelines.md": ["outline-real-lines", "outline-read-span"],
|
|
11
12
|
"replace-guidelines.md": ["replace-disjoint", "replace-deletion", "replace-single-line"],
|
|
12
13
|
"replace-match-guidelines.md": ["replace-match-fragment", "replace-match-wide-range", "replace-match-whole-range"],
|
|
13
14
|
"insert-guidelines.md": ["insert-blank-edges"],
|
package/src/read.ts
CHANGED
|
@@ -397,6 +397,7 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
397
397
|
const notices = [
|
|
398
398
|
hadUtf8DecodeErrors ? "[Non-UTF-8 bytes shown as U+FFFD; editing rewrites the file as UTF-8.]" : undefined,
|
|
399
399
|
reclaimNotice,
|
|
400
|
+
flags.outlineEnabled && preview.truncation !== undefined ? "[Read truncated; call `outline` on this file to see its structure.]" : undefined,
|
|
400
401
|
].filter((part): part is string => part !== undefined);
|
|
401
402
|
const previewText = [preview.text, ...notices].join("\n\n");
|
|
402
403
|
const servedRawText = preview.anchoredLines.map((row) => fileLines[row.line - 1] ?? "").join("\n");
|
package/src/structured.ts
CHANGED
|
@@ -82,6 +82,38 @@ export const readResultSchema = Type.Union([
|
|
|
82
82
|
structuredErrorSchema,
|
|
83
83
|
]);
|
|
84
84
|
|
|
85
|
+
export type OutlineStructured = {
|
|
86
|
+
ok: true;
|
|
87
|
+
kind: "outline";
|
|
88
|
+
path: string;
|
|
89
|
+
text: string;
|
|
90
|
+
totalLines: number;
|
|
91
|
+
depth: number;
|
|
92
|
+
lines: AnchoredLine[];
|
|
93
|
+
truncated: boolean;
|
|
94
|
+
complete: boolean;
|
|
95
|
+
notices: string[];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type OutlineResult = OutlineStructured | string | StructuredError;
|
|
99
|
+
|
|
100
|
+
export const outlineResultSchema = Type.Union([
|
|
101
|
+
Type.Object({
|
|
102
|
+
ok: Type.Literal(true),
|
|
103
|
+
kind: Type.Literal("outline"),
|
|
104
|
+
path: Type.String(),
|
|
105
|
+
text: Type.String(),
|
|
106
|
+
totalLines: Type.Integer(),
|
|
107
|
+
depth: Type.Integer(),
|
|
108
|
+
lines: Type.Array(anchoredLineSchema),
|
|
109
|
+
truncated: Type.Boolean(),
|
|
110
|
+
complete: Type.Boolean(),
|
|
111
|
+
notices: Type.Array(Type.String()),
|
|
112
|
+
}),
|
|
113
|
+
Type.String(),
|
|
114
|
+
structuredErrorSchema,
|
|
115
|
+
]);
|
|
116
|
+
|
|
85
117
|
export type EditVerb = "replaced" | "inserted" | "copied" | "moved" | "undone" | "edited";
|
|
86
118
|
|
|
87
119
|
export type EditStructured = {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Outline one file's structure by anchor: one row per symbol with the first line of its declaration, `[limit N]` for the span of a multi-line symbol
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Return a structural outline of one file: a `=== path (language) — N lines ===` header, then one `anchor│content` row per symbol, where the content is the first line of that symbol's declaration (annotations and attributes excluded). A `[limit N]` line under a multi-line symbol is the span to pass as `read`'s `limit` when reading that symbol's body from its anchor. A `[N nested]` line marks children hidden by `depth`. Files the parser cannot outline fall back to a head-and-tail preview. A row is editable with replace and insert when its line is shown whole; a row whose line is too long is clipped with a trailing `...` and is not served for editing, so read that line first.
|
|
2
|
+
|
|
3
|
+
The outline is a map, not the file: it shows each symbol's declaration line only, so read the span before editing inside a body. Anchors are lowercase letters and stay valid until their line is edited.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
- `read`: call again after an edit when you need anchors you lack — post-edit diff `+`/` ` rows and served `anchor│content` rows already carry fresh anchors for the changed range.
|
|
2
|
-
- `read`: an auto-read-all outline row
|
|
2
|
+
- `read`: an auto-read-all outline row is the first line of a symbol's declaration, and a `[limit N]` line under it is the span to request; a row clipped with a trailing `...` is not served for editing.
|
|
3
3
|
- `read`: `replace_match` needs both boundary lines served; copy `match` from a served row or read the lines first.
|