@d3ara1n/pi-hashline-edit 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/pi/edit-tool.ts +3 -1
- package/src/pi/grep-tool.ts +3 -2
- package/src/pi/read-tool.ts +90 -1
- package/src/pi/render.ts +30 -0
- package/src/pi/replace-tool.ts +3 -1
package/package.json
CHANGED
package/src/pi/edit-tool.ts
CHANGED
|
@@ -209,7 +209,9 @@ export function makeEditOverride(cwd: string) {
|
|
|
209
209
|
}
|
|
210
210
|
const diff: string | undefined = result.details?.diff;
|
|
211
211
|
if (!diff) {
|
|
212
|
-
|
|
212
|
+
// No net diff (e.g. a successful but non-mutating edit): show only the summary
|
|
213
|
+
// line — content.text also carries `Updated anchors` (hashline) for the model.
|
|
214
|
+
const t = content?.type === "text" ? content.text.split("\n")[0] : "Edited";
|
|
213
215
|
return new Text(theme.fg("success", t), 0, 0);
|
|
214
216
|
}
|
|
215
217
|
// details.diff is pi-format (+N/-N/<space>N content); color by leading char
|
package/src/pi/grep-tool.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { hashFileLines } from "../core/hash.ts";
|
|
|
33
33
|
import { splitLines } from "../core/lines.ts";
|
|
34
34
|
import { getState } from "./state.ts";
|
|
35
35
|
import { canonicalPath } from "./read-tool.ts";
|
|
36
|
+
import { parseHashline } from "./render.ts";
|
|
36
37
|
|
|
37
38
|
const DEFAULT_LIMIT = 100;
|
|
38
39
|
/** Max chars per result line for display (mirrors pi's truncate.ts; not exported there). */
|
|
@@ -89,9 +90,9 @@ function toDisplayLines(raw: string, theme: any): string[] {
|
|
|
89
90
|
const group: { lineNo: string; content: string }[] = [];
|
|
90
91
|
let j = i + 1;
|
|
91
92
|
while (j < lines.length) {
|
|
92
|
-
const a = lines[j]
|
|
93
|
+
const a = parseHashline(lines[j]);
|
|
93
94
|
if (!a) break;
|
|
94
|
-
group.push({ lineNo: a
|
|
95
|
+
group.push({ lineNo: a.lineNo, content: a.content });
|
|
95
96
|
j++;
|
|
96
97
|
}
|
|
97
98
|
// common base = min leading whitespace across the group; fold it into a marker
|
package/src/pi/read-tool.ts
CHANGED
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
* @module pi-hashline-edit/pi
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { createReadTool } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { createReadTool, getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
13
14
|
import { readFile } from "node:fs/promises";
|
|
14
15
|
import { join, resolve } from "node:path";
|
|
15
16
|
import { homedir } from "node:os";
|
|
16
17
|
import { hashFileLines } from "../core/hash.ts";
|
|
17
18
|
import { splitLines } from "../core/lines.ts";
|
|
18
19
|
import { getState } from "./state.ts";
|
|
20
|
+
import { parseHashline } from "./render.ts";
|
|
19
21
|
|
|
20
22
|
const MAX_LINES = 2000;
|
|
21
23
|
const MAX_BYTES = 256 * 1024;
|
|
@@ -37,6 +39,70 @@ function expandTilde(p: string): string {
|
|
|
37
39
|
return p;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
/** Offset/limit range suffix for the read call line, e.g. `:50-99` (mirrors pi core's read tool). */
|
|
43
|
+
function formatReadLineRange(args: any, theme: any): string {
|
|
44
|
+
if (args?.offset === undefined && args?.limit === undefined) return "";
|
|
45
|
+
const start = args.offset ?? 1;
|
|
46
|
+
const end = args.limit !== undefined ? start + args.limit - 1 : "";
|
|
47
|
+
return theme.fg("warning", `:${start}${end ? `-${end}` : ""}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Render the expanded read body for the TUI: color the header, strip the
|
|
52
|
+
* `LINE#HASH│` prefix from every anchor line to ` N: content`, and
|
|
53
|
+
* syntax-highlight the code block by the file's language (falls back to a
|
|
54
|
+
* single `toolOutput` color when the language is unknown or the highlight
|
|
55
|
+
* line count diverges). Trailing notices (e.g. truncation) are shown in
|
|
56
|
+
* `warning`.
|
|
57
|
+
*/
|
|
58
|
+
function renderReadBody(raw: string, path: string, theme: any): string {
|
|
59
|
+
const lines = raw.split("\n");
|
|
60
|
+
if (lines.length === 0) return "";
|
|
61
|
+
const out: string[] = [];
|
|
62
|
+
|
|
63
|
+
// Header: "<path> · <N> lines" optionally followed by " (from line <offset>)".
|
|
64
|
+
let bodyStart = 0;
|
|
65
|
+
const h = lines[0].match(/^(.+?) · (\d+ lines(?: \(from line \d+\))?)$/);
|
|
66
|
+
if (h) {
|
|
67
|
+
out.push(theme.fg("success", h[1]) + theme.fg("dim", ` · ${h[2]}`));
|
|
68
|
+
bodyStart = 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Collect anchor rows (full content); the first non-anchor line begins the tail.
|
|
72
|
+
const lineNos: string[] = [];
|
|
73
|
+
const codeContents: string[] = [];
|
|
74
|
+
let tailStart = lines.length;
|
|
75
|
+
for (let i = bodyStart; i < lines.length; i++) {
|
|
76
|
+
const row = parseHashline(lines[i]);
|
|
77
|
+
if (!row) {
|
|
78
|
+
tailStart = i;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
lineNos.push(row.lineNo);
|
|
82
|
+
codeContents.push(row.content);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Syntax-highlight the whole block so multi-line constructs stay correct.
|
|
86
|
+
const detabbed = codeContents.map((l) => l.replace(/\t/g, " "));
|
|
87
|
+
const lang = getLanguageFromPath(path);
|
|
88
|
+
let rendered: string[];
|
|
89
|
+
if (lang) {
|
|
90
|
+
const hl = highlightCode(detabbed.join("\n"), lang);
|
|
91
|
+
// Guard against highlighters that reshape line count: fall back to plain.
|
|
92
|
+
rendered = hl.length === detabbed.length ? hl : detabbed.map((l) => theme.fg("toolOutput", l));
|
|
93
|
+
} else {
|
|
94
|
+
rendered = detabbed.map((l) => theme.fg("toolOutput", l));
|
|
95
|
+
}
|
|
96
|
+
for (let i = 0; i < rendered.length && i < lineNos.length; i++) {
|
|
97
|
+
out.push(theme.fg("dim", ` ${lineNos[i]}: `) + rendered[i]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (let i = tailStart; i < lines.length; i++) {
|
|
101
|
+
out.push(theme.fg("warning", lines[i]));
|
|
102
|
+
}
|
|
103
|
+
return out.join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
40
106
|
/** Build the read override (a ToolDefinition fragment for registerTool). */
|
|
41
107
|
export function makeReadOverride(cwd: string) {
|
|
42
108
|
const builtin = createReadTool(cwd);
|
|
@@ -52,6 +118,29 @@ export function makeReadOverride(cwd: string) {
|
|
|
52
118
|
"Pass `path`; optionally `offset` (1-indexed start line) and `limit` (max lines). Prefer read over cat/sed for files you intend to edit.",
|
|
53
119
|
],
|
|
54
120
|
parameters: builtin.parameters,
|
|
121
|
+
renderShell: "default" as const,
|
|
122
|
+
|
|
123
|
+
renderCall(args: any, theme: any) {
|
|
124
|
+
const pathDisplay = String(args?.path ?? "");
|
|
125
|
+
let text = theme.fg("toolTitle", theme.bold("read")) + " " + theme.fg("accent", pathDisplay);
|
|
126
|
+
const range = formatReadLineRange(args, theme);
|
|
127
|
+
if (range) text += range;
|
|
128
|
+
return new Text(text, 0, 0);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
renderResult(result: any, { isPartial, expanded }: any, theme: any, context: any) {
|
|
132
|
+
if (isPartial) return new Text(theme.fg("warning", "Reading…"), 0, 0);
|
|
133
|
+
const content = result.content?.[0];
|
|
134
|
+
if (context?.isError) {
|
|
135
|
+
const t = content?.type === "text" ? content.text.split("\n")[0] : "Error";
|
|
136
|
+
return new Text(theme.fg("error", t), 0, 0);
|
|
137
|
+
}
|
|
138
|
+
// Collapsed (not expanded): show nothing — the call line carries the
|
|
139
|
+
// title, matching the built-in read's fold behavior.
|
|
140
|
+
if (!expanded) return new Text("", 0, 0);
|
|
141
|
+
const raw = content?.type === "text" ? content.text : "";
|
|
142
|
+
return new Text(renderReadBody(raw, String(context?.args?.path ?? ""), theme), 0, 0);
|
|
143
|
+
},
|
|
55
144
|
|
|
56
145
|
async execute(toolCallId: string, params: any, signal: AbortSignal | undefined, onUpdate: any) {
|
|
57
146
|
// Not enabled OR user cancelled → delegate to the built-in (builtin handles abort itself)
|
package/src/pi/render.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared anchor-parsing helper for the hashline-aware tool renderers.
|
|
3
|
+
*
|
|
4
|
+
* The model-facing `content` text uses the `LINE#HASH│content` anchor format so
|
|
5
|
+
* an anchor can be copied straight into an edit op. The user-facing TUI
|
|
6
|
+
* renderers (read, grep) strip that prefix back to a clean ` N: content` form.
|
|
7
|
+
* Parsing the anchor format in one place keeps read and grep in sync.
|
|
8
|
+
*
|
|
9
|
+
* @module pi-hashline-edit/pi
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const HASHLINE_RE = /^(\d+)#[A-Za-z0-9]+│(.*)$/;
|
|
13
|
+
|
|
14
|
+
export interface HashlineRow {
|
|
15
|
+
/** Line number as written in the anchor (string form). */
|
|
16
|
+
lineNo: string;
|
|
17
|
+
/** Line content with the `LINE#HASH│` prefix removed. */
|
|
18
|
+
content: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a `LINE#HASH│content` anchor line.
|
|
23
|
+
*
|
|
24
|
+
* @returns the row, or `null` for non-anchor lines (headers, notices, free
|
|
25
|
+
* text) so callers can fall through to their own formatting.
|
|
26
|
+
*/
|
|
27
|
+
export function parseHashline(line: string): HashlineRow | null {
|
|
28
|
+
const m = line.match(HASHLINE_RE);
|
|
29
|
+
return m ? { lineNo: m[1], content: m[2] } : null;
|
|
30
|
+
}
|
package/src/pi/replace-tool.ts
CHANGED
|
@@ -180,7 +180,9 @@ export function makeReplaceTool(cwd: string) {
|
|
|
180
180
|
}
|
|
181
181
|
const diff: string | undefined = result.details?.diff;
|
|
182
182
|
if (!diff) {
|
|
183
|
-
|
|
183
|
+
// No net diff: show only the summary line — content.text also carries
|
|
184
|
+
// `Updated anchors` (hashline) for the model.
|
|
185
|
+
const t = content?.type === "text" ? content.text.split("\n")[0] : "Replaced";
|
|
184
186
|
return new Text(theme.fg("success", t), 0, 0);
|
|
185
187
|
}
|
|
186
188
|
// details.diff is pi-format (+N/-N/<space>N content); color by leading char
|