pi-ast-sgrep 2.0.2 → 2.1.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/README.md +15 -19
- package/dist/code-mode.d.ts +1 -1
- package/dist/code-mode.js +1 -1
- package/dist/codemode/connector.d.ts +17 -3
- package/dist/codemode/connector.js +59 -31
- package/dist/codemode/dispatch.d.ts +13 -1
- package/dist/codemode/dispatch.js +83 -23
- package/dist/codemode/guest-api.d.ts +16 -0
- package/dist/codemode/guest-api.js +194 -0
- package/dist/codemode/guest-worker.mjs +287 -0
- package/dist/codemode/index.d.ts +4 -3
- package/dist/codemode/index.js +4 -3
- package/dist/codemode/runner.d.ts +13 -9
- package/dist/codemode/runner.js +411 -213
- package/dist/codemode/session-pool.d.ts +6 -1
- package/dist/codemode/session-pool.js +125 -32
- package/dist/codemode/types.d.ts +33 -2
- package/dist/codemode/types.js +39 -13
- package/dist/codemode/worker.d.ts +1 -1
- package/dist/codemode/worker.js +25 -2
- package/dist/host/commands.d.ts +6 -0
- package/dist/host/commands.js +49 -0
- package/dist/host/results.d.ts +115 -0
- package/dist/host/results.js +80 -0
- package/dist/host/tools.d.ts +9 -0
- package/dist/host/tools.js +610 -0
- package/dist/index.d.ts +7 -34
- package/dist/index.js +5 -543
- package/dist/runtime/config.d.ts +36 -0
- package/dist/runtime/config.js +98 -0
- package/dist/runtime/freshness.d.ts +43 -0
- package/dist/runtime/freshness.js +413 -0
- package/dist/runtime/index-health.d.ts +16 -0
- package/dist/runtime/index-health.js +109 -0
- package/dist/runtime/runtime.d.ts +48 -0
- package/dist/runtime/runtime.js +265 -0
- package/dist/runtime/sqlite.d.ts +15 -0
- package/dist/runtime/sqlite.js +63 -0
- package/dist/runtime/types.d.ts +55 -0
- package/dist/runtime/types.js +25 -0
- package/dist/ui/card.d.ts +63 -0
- package/dist/ui/card.js +316 -0
- package/dist/{present.d.ts → ui/present.d.ts} +20 -2
- package/dist/{present.js → ui/present.js} +68 -9
- package/package.json +6 -6
- package/dist/codemode/sandbox-worker.d.ts +0 -1
- package/dist/codemode/sandbox-worker.js +0 -204
- package/dist/runtime.d.ts +0 -137
- package/dist/runtime.js +0 -799
package/dist/ui/card.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/** Pi TUI result card — supernova-style: the call slot is empty; one card
|
|
2
|
+
* owns the whole lifecycle (running → ops ledger → result → error). Rows are
|
|
3
|
+
* fixed-column and theme-painted; nothing here writes to the model channel. */
|
|
4
|
+
import { hitLabel, hitLocation, paint, truncateToWidth, visibleWidth, } from "./present.js";
|
|
5
|
+
/** Header text that rides the top border: "asgrep search · 4 hits · 12ms · napi". */
|
|
6
|
+
function frameLabel(model) {
|
|
7
|
+
const bits = ["asgrep", model.command, ...model.title.filter((b) => Boolean(b))];
|
|
8
|
+
const tail = model.error ? "failed" : model.running ? "running" : "";
|
|
9
|
+
if (tail)
|
|
10
|
+
bits.push(tail);
|
|
11
|
+
const counts = [];
|
|
12
|
+
if (model.ops?.length)
|
|
13
|
+
counts.push(model.ops.length + (model.ops.length === 1 ? " call" : " calls"));
|
|
14
|
+
if (model.hits)
|
|
15
|
+
counts.push(model.hits.length + (model.hits.length === 1 ? " hit" : " hits"));
|
|
16
|
+
return [...bits.slice(0, 2), ...counts, ...bits.slice(2)].join(" \u00b7 ");
|
|
17
|
+
}
|
|
18
|
+
/** renderCall component that paints nothing — the result card owns display. */
|
|
19
|
+
export const EMPTY_CALL = {
|
|
20
|
+
render: () => [],
|
|
21
|
+
invalidate() { },
|
|
22
|
+
};
|
|
23
|
+
const TOOL_COL = 12;
|
|
24
|
+
const DUR_COL = 7;
|
|
25
|
+
const MAX_CARD_WIDTH = 100;
|
|
26
|
+
/** Rounded frame glyphs — pi themes may provide theme.boxRound; default ASCII-art set. */
|
|
27
|
+
const BOX = { tl: "\u256d", tr: "\u256e", bl: "\u2570", br: "\u256f", h: "\u2500", v: "\u2502" };
|
|
28
|
+
function boxOf(theme) {
|
|
29
|
+
const b = theme?.boxRound;
|
|
30
|
+
if (b && typeof b.topLeft === "string" && typeof b.horizontal === "string" && typeof b.vertical === "string") {
|
|
31
|
+
return { tl: b.topLeft, tr: b.topRight ?? BOX.tr, bl: b.bottomLeft ?? BOX.bl, br: b.bottomRight ?? BOX.br, h: b.horizontal, v: b.vertical };
|
|
32
|
+
}
|
|
33
|
+
return BOX;
|
|
34
|
+
}
|
|
35
|
+
function borderKey(model) {
|
|
36
|
+
if (model.error)
|
|
37
|
+
return "error";
|
|
38
|
+
if (model.running)
|
|
39
|
+
return "accent";
|
|
40
|
+
return "dim";
|
|
41
|
+
}
|
|
42
|
+
/** Top/bottom bar with an optional label embedded in the rule. Geometry is in
|
|
43
|
+
* display cells (frameW): \u256d + 3 rules on the left, corner on the right. */
|
|
44
|
+
function frameBar(theme, box, border, left, right, label, width) {
|
|
45
|
+
const leftRaw = left + box.h.repeat(3);
|
|
46
|
+
const shown = label ? clamp(" " + label + " ", Math.max(0, width - frameW(leftRaw) - 1)) : "";
|
|
47
|
+
const fill = Math.max(0, width - frameW(leftRaw) - frameW(shown) - 1);
|
|
48
|
+
return border(leftRaw) + shown + border(box.h.repeat(fill)) + border(right);
|
|
49
|
+
}
|
|
50
|
+
/** ANSI-aware cut to `width` display columns on the same scale as frameW
|
|
51
|
+
* (stripped code-point count). Unlike truncateToWidth this never leaves an
|
|
52
|
+
* unclosed SGR color behind: a cut inside a painted span appends \x1b[0m
|
|
53
|
+
* before the ellipsis so the row cannot bleed color into the right border. */
|
|
54
|
+
function clamp(text, width) {
|
|
55
|
+
const limit = Math.max(1, width);
|
|
56
|
+
if (frameW(text) <= limit)
|
|
57
|
+
return text;
|
|
58
|
+
const budget = Math.max(1, limit - 1); // room for the ellipsis
|
|
59
|
+
const ansi = /\u001b\[[0-9;]*m/gu;
|
|
60
|
+
const stops = [];
|
|
61
|
+
for (let match = ansi.exec(text); match !== null; match = ansi.exec(text)) {
|
|
62
|
+
stops.push([match.index, match.index + match[0].length]);
|
|
63
|
+
}
|
|
64
|
+
let kept = "";
|
|
65
|
+
let visible = 0;
|
|
66
|
+
let index = 0;
|
|
67
|
+
let stopIndex = 0;
|
|
68
|
+
while (index < text.length && visible < budget) {
|
|
69
|
+
if (stopIndex < stops.length && index === stops[stopIndex][0]) {
|
|
70
|
+
const [, end] = stops[stopIndex];
|
|
71
|
+
kept += text.slice(index, end);
|
|
72
|
+
index = end;
|
|
73
|
+
stopIndex += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
kept += text[index];
|
|
77
|
+
visible += 1;
|
|
78
|
+
index += 1;
|
|
79
|
+
}
|
|
80
|
+
return kept + (stops.length > 0 ? "\u001b[0m" : "") + "\u2026";
|
|
81
|
+
}
|
|
82
|
+
function fitPath(text, budget) {
|
|
83
|
+
if (visibleWidth(text) <= budget)
|
|
84
|
+
return text;
|
|
85
|
+
if (budget <= 1)
|
|
86
|
+
return "\u2026";
|
|
87
|
+
return "\u2026" + text.slice(Math.max(0, text.length - budget + 1));
|
|
88
|
+
}
|
|
89
|
+
/** Display columns for frame geometry: ANSI-stripped code-point count.
|
|
90
|
+
* Box glyphs/·/✓ render width-1 in real terminals; the conservative
|
|
91
|
+
* visibleWidth() over-counts them (width 2) which would ragged the box. */
|
|
92
|
+
function frameW(text) {
|
|
93
|
+
return text.replace(/\u001b\[[0-9;]*m/g, "").length;
|
|
94
|
+
}
|
|
95
|
+
function fmtMs(ms) {
|
|
96
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
97
|
+
return "";
|
|
98
|
+
return ms < 1000 ? Math.round(ms) + "ms" : (ms / 1000).toFixed(1) + "s";
|
|
99
|
+
}
|
|
100
|
+
/** "✓ search 12ms \"query\"" — one aligned op row. */
|
|
101
|
+
function opRow(theme, op, width) {
|
|
102
|
+
const marker = op.ok ? paint(theme, "success", "\u2713") : paint(theme, "error", "\u00d7");
|
|
103
|
+
const tool = paint(theme, "syntaxFunction", op.tool.slice(0, TOOL_COL).padEnd(TOOL_COL));
|
|
104
|
+
const dur = paint(theme, "dim", fmtMs(op.ms).padStart(DUR_COL));
|
|
105
|
+
const target = op.target ? " " + paint(theme, "muted", fitPath(op.target, Math.max(1, width - TOOL_COL - DUR_COL - 9))) : "";
|
|
106
|
+
return " " + marker + " " + tool + " " + dur + target;
|
|
107
|
+
}
|
|
108
|
+
/** " 1. path:line symbol · kind" — one numbered hit row. */
|
|
109
|
+
function hitRow(theme, n, hit, width) {
|
|
110
|
+
const num = paint(theme, "dim", String(n).padStart(2) + ".");
|
|
111
|
+
const loc = hitLocation(hit);
|
|
112
|
+
const locBudget = Math.max(8, Math.floor(width * 0.62));
|
|
113
|
+
const locText = paint(theme, "accent", fitPath(loc, locBudget));
|
|
114
|
+
const label = hitLabel(hit);
|
|
115
|
+
const row = " " + num + " " + locText + (label ? " " + paint(theme, "muted", label) : "");
|
|
116
|
+
return clamp(row, width);
|
|
117
|
+
}
|
|
118
|
+
export class AsgrepCard {
|
|
119
|
+
theme;
|
|
120
|
+
model;
|
|
121
|
+
cache;
|
|
122
|
+
set(theme, model) {
|
|
123
|
+
this.theme = theme;
|
|
124
|
+
this.model = model;
|
|
125
|
+
this.cache = undefined;
|
|
126
|
+
}
|
|
127
|
+
invalidate() {
|
|
128
|
+
this.cache = undefined;
|
|
129
|
+
}
|
|
130
|
+
render(width = 80) {
|
|
131
|
+
const theme = this.theme;
|
|
132
|
+
const model = this.model;
|
|
133
|
+
if (!model || width <= 0)
|
|
134
|
+
return [];
|
|
135
|
+
if (this.cache?.width === width)
|
|
136
|
+
return this.cache.lines;
|
|
137
|
+
// Pi hands us the full terminal width — a hollow frame at 200+ cols is a
|
|
138
|
+
// wall of empty border. Cap at a readable card width; pi pads the rest.
|
|
139
|
+
const lines = framedLines(theme, model, Math.min(Math.max(8, width), MAX_CARD_WIDTH));
|
|
140
|
+
this.cache = { width, lines };
|
|
141
|
+
return lines;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Rounded card: header embedded in the top rule, body rows in \u2502 gutters. */
|
|
145
|
+
function framedLines(theme, model, width) {
|
|
146
|
+
const box = boxOf(theme);
|
|
147
|
+
const key = borderKey(model);
|
|
148
|
+
const border = (text) => paint(theme, key, text);
|
|
149
|
+
const inner = Math.max(1, width - 4); // "\u2502 " + content + " \u2502"
|
|
150
|
+
const label = frameLabel(model);
|
|
151
|
+
const rows = bodyLines(theme, model, inner);
|
|
152
|
+
const out = [frameBar(theme, box, border, box.tl, box.tr, label, width)];
|
|
153
|
+
for (const row of rows) {
|
|
154
|
+
const body = clamp(row, inner);
|
|
155
|
+
const pad = Math.max(0, inner - frameW(body));
|
|
156
|
+
out.push(border(box.v) + " " + body + " ".repeat(pad) + " " + border(box.v));
|
|
157
|
+
}
|
|
158
|
+
out.push(frameBar(theme, box, border, box.bl, box.br, null, width));
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
function bodyLines(theme, model, width) {
|
|
162
|
+
const lines = [];
|
|
163
|
+
const maxOps = model.expanded ? 24 : 8;
|
|
164
|
+
const maxHits = model.expanded ? 24 : 12;
|
|
165
|
+
const ops = (model.ops ?? []).slice(0, maxOps);
|
|
166
|
+
for (const op of ops)
|
|
167
|
+
lines.push(clamp(opRow(theme, op, width), width));
|
|
168
|
+
if ((model.ops?.length ?? 0) > ops.length) {
|
|
169
|
+
lines.push(paint(theme, "dim", " \u2026 " + ((model.ops?.length ?? 0) - ops.length) + " more calls"));
|
|
170
|
+
}
|
|
171
|
+
const hits = (model.hits ?? []).slice(0, maxHits);
|
|
172
|
+
hits.forEach((hit, i) => lines.push(hitRow(theme, i + 1, hit, width)));
|
|
173
|
+
if ((model.hits?.length ?? 0) > hits.length) {
|
|
174
|
+
lines.push(paint(theme, "dim", " \u2026 " + ((model.hits?.length ?? 0) - hits.length) + " more"));
|
|
175
|
+
}
|
|
176
|
+
for (const edit of (model.edits ?? []).slice(0, model.expanded ? 12 : 4)) {
|
|
177
|
+
const head = " " + paint(theme, "accent", edit.path ?? "?") + (edit.line ? paint(theme, "dim", ":" + edit.line) : "");
|
|
178
|
+
lines.push(clamp(head, width));
|
|
179
|
+
for (const line of (edit.removed ?? []).slice(0, model.expanded ? 24 : 8)) {
|
|
180
|
+
lines.push(" " + paint(theme, "error", "- " + clamp(line, width - 4)));
|
|
181
|
+
}
|
|
182
|
+
for (const line of (edit.added ?? []).slice(0, model.expanded ? 24 : 8)) {
|
|
183
|
+
lines.push(" " + paint(theme, "success", "+ " + clamp(line, width - 4)));
|
|
184
|
+
}
|
|
185
|
+
if (edit.truncated)
|
|
186
|
+
lines.push(paint(theme, "dim", " \u2026"));
|
|
187
|
+
}
|
|
188
|
+
if (model.error) {
|
|
189
|
+
for (const line of model.error.split("\n").slice(0, model.expanded ? 12 : 4)) {
|
|
190
|
+
lines.push(" " + paint(theme, "error", clamp(line, width - 1)));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (model.resultLines && model.resultLines.length > 0) {
|
|
194
|
+
const shown = model.expanded ? model.resultLines : model.resultLines.slice(0, 8);
|
|
195
|
+
for (const line of shown)
|
|
196
|
+
lines.push(" " + paint(theme, "muted", clamp(line, width - 1)));
|
|
197
|
+
if (!model.expanded && model.resultLines.length > shown.length) {
|
|
198
|
+
lines.push(paint(theme, "dim", " \u2026 " + (model.resultLines.length - shown.length) + " more result lines"));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return lines;
|
|
202
|
+
}
|
|
203
|
+
function editsOf(value) {
|
|
204
|
+
if (value && typeof value === "object" && Array.isArray(value.edits)) {
|
|
205
|
+
return value.edits.filter((e) => !!e && typeof e === "object" && (Array.isArray(e.removed) || Array.isArray(e.added)));
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
function hitsOf(value) {
|
|
210
|
+
if (value && typeof value === "object" && Array.isArray(value.hits)) {
|
|
211
|
+
return value.hits;
|
|
212
|
+
}
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
function resultPreviewLines(value) {
|
|
216
|
+
if (value === undefined || value === null)
|
|
217
|
+
return undefined;
|
|
218
|
+
if (typeof value === "string")
|
|
219
|
+
return value.split("\n").filter((l) => l.length > 0).slice(0, 16);
|
|
220
|
+
if (Array.isArray(value))
|
|
221
|
+
return [value.length + " value" + (value.length === 1 ? "" : "s")];
|
|
222
|
+
if (typeof value === "object") {
|
|
223
|
+
return Object.entries(value).slice(0, 12).map(([k, v]) => {
|
|
224
|
+
const text = typeof v === "string" ? v : JSON.stringify(v);
|
|
225
|
+
return k + ": " + (text.length > 80 ? text.slice(0, 79) + "\u2026" : text);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return [String(value)];
|
|
229
|
+
}
|
|
230
|
+
/** Build the card model from the tool result's details payload. */
|
|
231
|
+
export function cardModel(result, options) {
|
|
232
|
+
const details = (result.details && typeof result.details === "object" ? result.details : {});
|
|
233
|
+
const command = typeof details.command === "string" ? details.command : "asgrep";
|
|
234
|
+
const expanded = options.expanded === true;
|
|
235
|
+
// In-flight partial updates carry only {command, phase}.
|
|
236
|
+
if (options.isPartial && !("ok" in details)) {
|
|
237
|
+
return { command, title: [typeof details.phase === "string" ? details.phase : undefined], running: true, expanded };
|
|
238
|
+
}
|
|
239
|
+
const title = [];
|
|
240
|
+
if (typeof details.query === "string" && details.query)
|
|
241
|
+
title.push(JSON.stringify(details.query));
|
|
242
|
+
if (typeof details.mode === "string")
|
|
243
|
+
title.push(details.mode);
|
|
244
|
+
const error = details.error;
|
|
245
|
+
if (result.isError || details.ok === false || error) {
|
|
246
|
+
return {
|
|
247
|
+
command,
|
|
248
|
+
title,
|
|
249
|
+
error: typeof error?.message === "string" ? error.message : "tool failed",
|
|
250
|
+
expanded,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const response = details.response;
|
|
254
|
+
if (command === "status" || command === "index" || command === "reindex") {
|
|
255
|
+
const state = typeof response?.status === "string" ? response.status
|
|
256
|
+
: typeof response?.index_status === "string" ? response.index_status
|
|
257
|
+
: response?.ok === true ? "ok" : undefined;
|
|
258
|
+
if (state)
|
|
259
|
+
title.push(state);
|
|
260
|
+
const counts = response?.counts;
|
|
261
|
+
if (counts && typeof counts === "object") {
|
|
262
|
+
title.push(Object.entries(counts).slice(0, 4).map(([k, v]) => k + "=" + String(v)).join(" "));
|
|
263
|
+
}
|
|
264
|
+
else if (response) {
|
|
265
|
+
// Flat status envelope: file_count/symbol_count/caller_count + embed info.
|
|
266
|
+
const flat = [];
|
|
267
|
+
for (const k of ["file_count", "symbol_count", "caller_count"]) {
|
|
268
|
+
const v = response[k];
|
|
269
|
+
if (typeof v === "number" || typeof v === "bigint")
|
|
270
|
+
flat.push(k.replace(/_count$/, "s") + "=" + String(v));
|
|
271
|
+
}
|
|
272
|
+
if (typeof response.embed_backend === "string")
|
|
273
|
+
flat.push(response.embed_backend);
|
|
274
|
+
if (flat.length > 0)
|
|
275
|
+
title.push(flat.join(" "));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const ms = typeof details.wallMs === "number" ? details.wallMs : typeof details.activationMs === "number" ? details.activationMs : undefined;
|
|
279
|
+
if (ms !== undefined)
|
|
280
|
+
title.push(fmtMs(ms));
|
|
281
|
+
if (typeof details.backend === "string")
|
|
282
|
+
title.push(details.backend);
|
|
283
|
+
const trace = Array.isArray(details.trace) ? details.trace : undefined;
|
|
284
|
+
const ops = trace?.map((t) => ({ tool: t.tool, target: t.target ?? "", ok: t.ok !== false, ms: typeof t.ms === "number" ? t.ms : 0 }));
|
|
285
|
+
const hits = hitsOf(response) ?? hitsOf(details.result);
|
|
286
|
+
const resultEdits = editsOf(details.result) ?? editsOf(response);
|
|
287
|
+
// read envelopes carry windows: preview each window's first lines.
|
|
288
|
+
const windows = command === "read" && response && Array.isArray(response.windows)
|
|
289
|
+
? response.windows
|
|
290
|
+
: undefined;
|
|
291
|
+
const readLines = windows?.flatMap((w) => [
|
|
292
|
+
(w.path ?? "?") + ":" + (w.start ?? 1) + "-" + (w.end ?? ""),
|
|
293
|
+
...(typeof w.text === "string" ? w.text.split("\n").slice(0, expanded ? 20 : 6).map((l) => " " + l) : []),
|
|
294
|
+
]);
|
|
295
|
+
// When edits carry diffs they are the interesting part of the result.
|
|
296
|
+
const resultLines = hits || resultEdits ? undefined : (readLines ?? resultPreviewLines(details.result));
|
|
297
|
+
const model = { command, title, expanded };
|
|
298
|
+
if (ops && ops.length > 0)
|
|
299
|
+
model.ops = ops;
|
|
300
|
+
if (hits)
|
|
301
|
+
model.hits = hits;
|
|
302
|
+
if (resultEdits)
|
|
303
|
+
model.edits = resultEdits;
|
|
304
|
+
if (resultLines)
|
|
305
|
+
model.resultLines = resultLines;
|
|
306
|
+
return model;
|
|
307
|
+
}
|
|
308
|
+
/** renderResult entrypoint: bind one card per result slot, feed it details. */
|
|
309
|
+
export function renderAsgrepResult(result, options, theme, context) {
|
|
310
|
+
const prev = context?.lastComponent;
|
|
311
|
+
const card = prev instanceof AsgrepCard ? prev : new AsgrepCard();
|
|
312
|
+
card.set(theme, cardModel(result, options));
|
|
313
|
+
if (context)
|
|
314
|
+
context.lastComponent = card;
|
|
315
|
+
return card;
|
|
316
|
+
}
|
|
@@ -25,8 +25,12 @@ export type EnvelopeLike = {
|
|
|
25
25
|
backend?: unknown;
|
|
26
26
|
[key: string]: unknown;
|
|
27
27
|
};
|
|
28
|
-
export declare const ASGREP_PROMPT_SNIPPET = "Search this repo by intent, symbol, callers, defs,
|
|
29
|
-
export declare const ASGREP_PROMPT_GUIDELINES: readonly ["For any code lookup (find a function, callers, defs, intent, structural pattern
|
|
28
|
+
export declare const ASGREP_PROMPT_SNIPPET = "Search this repo by intent, symbol, callers, defs, or pattern (asgrep; use without being asked)";
|
|
29
|
+
export declare const ASGREP_PROMPT_GUIDELINES: readonly ["For any code lookup (find a function, callers, defs, intent, or structural pattern), call asgrep immediately. Do not wait for the user to mention ast-sgrep.", "Prefer the asgrep Code Mode tool. Write JavaScript: asgrep.search(\"query\"), asgrep.defs(\"Symbol\"), asgrep.callers(\"Symbol\"), asgrep.read({ refs }). Independent lookups: Promise.all. Return a small shaped value.", "Use grep only for exact log strings, filenames, or config keys. asgrep.edit does unique string replace plus targeted reindex; oldText must match exactly once.", "If a search returns 0 hits, use suggested_next or retry with asgrep.find, asgrep.defs, or asgrep.search(query, { in: \"src\" })."];
|
|
30
|
+
export declare function paint(theme: PresentTheme | undefined, role: string, text: string, bold?: boolean): string;
|
|
31
|
+
export declare function hitLocation(hit: HitLike): string;
|
|
32
|
+
export declare function hitLabel(hit: HitLike): string;
|
|
33
|
+
export declare function header(theme: PresentTheme | undefined, verb: string, bits: Array<string | null | undefined>): string;
|
|
30
34
|
export declare function formatSearchCall(params: {
|
|
31
35
|
query?: string;
|
|
32
36
|
mode?: string;
|
|
@@ -35,6 +39,20 @@ export declare function formatSearchCall(params: {
|
|
|
35
39
|
}, theme?: PresentTheme): string;
|
|
36
40
|
export declare function formatIndexCall(force: boolean, theme?: PresentTheme): string;
|
|
37
41
|
export declare function formatStatusCall(theme?: PresentTheme): string;
|
|
42
|
+
export declare function formatEditCall(params: {
|
|
43
|
+
path?: string;
|
|
44
|
+
edits?: unknown[];
|
|
45
|
+
}, theme?: PresentTheme): string;
|
|
46
|
+
export declare function formatReadCall(params: {
|
|
47
|
+
path?: string;
|
|
48
|
+
ref?: string;
|
|
49
|
+
start?: number;
|
|
50
|
+
end?: number;
|
|
51
|
+
}, theme?: PresentTheme): string;
|
|
52
|
+
/** Model-visible text for an edit envelope: what changed, per file. */
|
|
53
|
+
export declare function formatEditResult(response: EnvelopeLike, theme?: PresentTheme): string;
|
|
54
|
+
/** Model-visible text for a read envelope: the window contents themselves. */
|
|
55
|
+
export declare function formatReadResult(response: EnvelopeLike, theme?: PresentTheme): string;
|
|
38
56
|
export declare function formatCodemodeCall(code: string, theme?: PresentTheme): string;
|
|
39
57
|
export declare function formatSearchResult(response: EnvelopeLike, meta: {
|
|
40
58
|
command: string;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/** Neat asgrep tool chrome for the Pi TUI and the model-visible content. */
|
|
2
|
-
export const ASGREP_PROMPT_SNIPPET = "Search this repo by intent, symbol, callers, defs,
|
|
2
|
+
export const ASGREP_PROMPT_SNIPPET = "Search this repo by intent, symbol, callers, defs, or pattern (asgrep; use without being asked)";
|
|
3
3
|
export const ASGREP_PROMPT_GUIDELINES = [
|
|
4
|
-
"For any code lookup (find a function, callers, defs, intent, structural pattern
|
|
5
|
-
"Prefer the asgrep Code Mode tool
|
|
6
|
-
"Use grep only for exact log strings, filenames, or config keys.
|
|
4
|
+
"For any code lookup (find a function, callers, defs, intent, or structural pattern), call asgrep immediately. Do not wait for the user to mention ast-sgrep.",
|
|
5
|
+
"Prefer the asgrep Code Mode tool. Write JavaScript: asgrep.search(\"query\"), asgrep.defs(\"Symbol\"), asgrep.callers(\"Symbol\"), asgrep.read({ refs }). Independent lookups: Promise.all. Return a small shaped value.",
|
|
6
|
+
"Use grep only for exact log strings, filenames, or config keys. asgrep.edit does unique string replace plus targeted reindex; oldText must match exactly once.",
|
|
7
|
+
"If a search returns 0 hits, use suggested_next or retry with asgrep.find, asgrep.defs, or asgrep.search(query, { in: \"src\" }).",
|
|
7
8
|
];
|
|
8
|
-
function paint(theme, role, text, bold = false) {
|
|
9
|
+
export function paint(theme, role, text, bold = false) {
|
|
9
10
|
const body = bold && theme ? theme.bold(text) : text;
|
|
10
11
|
return theme ? theme.fg(role, body) : body;
|
|
11
12
|
}
|
|
12
|
-
function hitLocation(hit) {
|
|
13
|
+
export function hitLocation(hit) {
|
|
13
14
|
const file = String(hit.file ?? hit.path ?? "");
|
|
14
15
|
const line = hit.start_line ?? hit.line ?? hit.lines;
|
|
15
16
|
if (typeof line === "number")
|
|
@@ -20,13 +21,13 @@ function hitLocation(hit) {
|
|
|
20
21
|
return hit.ref;
|
|
21
22
|
return file || "?";
|
|
22
23
|
}
|
|
23
|
-
function hitLabel(hit) {
|
|
24
|
+
export function hitLabel(hit) {
|
|
24
25
|
const symbol = typeof hit.symbol === "string" ? hit.symbol : "";
|
|
25
26
|
const kind = typeof hit.kind === "string" ? hit.kind : "";
|
|
26
27
|
const preview = typeof hit.preview === "string" ? hit.preview.replace(/\s+/g, " ").trim() : "";
|
|
27
28
|
return [symbol, kind, preview && preview.length < 80 ? preview : ""].filter(Boolean).join(" ");
|
|
28
29
|
}
|
|
29
|
-
function header(theme, verb, bits) {
|
|
30
|
+
export function header(theme, verb, bits) {
|
|
30
31
|
return [paint(theme, "toolTitle", "asgrep", true), paint(theme, "accent", verb), ...bits.filter((bit) => Boolean(bit))].join(" · ");
|
|
31
32
|
}
|
|
32
33
|
export function formatSearchCall(params, theme) {
|
|
@@ -43,6 +44,44 @@ export function formatIndexCall(force, theme) {
|
|
|
43
44
|
export function formatStatusCall(theme) {
|
|
44
45
|
return header(theme, "status", []);
|
|
45
46
|
}
|
|
47
|
+
export function formatEditCall(params, theme) {
|
|
48
|
+
const n = Array.isArray(params.edits) ? params.edits.length : 0;
|
|
49
|
+
return header(theme, "edit", [params.path, n > 1 ? n + " edits" : undefined]);
|
|
50
|
+
}
|
|
51
|
+
export function formatReadCall(params, theme) {
|
|
52
|
+
const target = params.path ?? params.ref;
|
|
53
|
+
const range = params.start !== undefined ? "L" + params.start + "-L" + (params.end ?? "") : undefined;
|
|
54
|
+
return header(theme, "read", [target, range]);
|
|
55
|
+
}
|
|
56
|
+
/** Model-visible text for an edit envelope: what changed, per file. */
|
|
57
|
+
export function formatEditResult(response, theme) {
|
|
58
|
+
const edits = Array.isArray(response.edits) ? response.edits : [];
|
|
59
|
+
const changed = edits.filter((e) => e.changed === true).length;
|
|
60
|
+
const title = header(theme, "edit", [changed + "/" + edits.length + " changed"]);
|
|
61
|
+
const rows = edits.slice(0, 12).map((e) => {
|
|
62
|
+
const path = typeof e.path === "string" ? e.path : "?";
|
|
63
|
+
const line = typeof e.line === "number" ? ":" + e.line : "";
|
|
64
|
+
return paint(theme, "toolOutput", " " + path + line);
|
|
65
|
+
});
|
|
66
|
+
return [title, ...rows].join("\n");
|
|
67
|
+
}
|
|
68
|
+
/** Model-visible text for a read envelope: the window contents themselves. */
|
|
69
|
+
export function formatReadResult(response, theme) {
|
|
70
|
+
const windows = Array.isArray(response.windows) ? response.windows : [];
|
|
71
|
+
if (windows.length === 0)
|
|
72
|
+
return header(theme, "read", ["0 windows"]);
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const w of windows.slice(0, 8)) {
|
|
75
|
+
const path = typeof w.path === "string" ? w.path : "?";
|
|
76
|
+
out.push(header(theme, "read", [path + "#L" + (w.start ?? 1) + "-L" + (w.end ?? "")]));
|
|
77
|
+
const text = typeof w.text === "string" ? w.text : "";
|
|
78
|
+
for (const line of text.split("\n").slice(0, 80))
|
|
79
|
+
out.push(line);
|
|
80
|
+
}
|
|
81
|
+
if (windows.length > 8)
|
|
82
|
+
out.push("… " + (windows.length - 8) + " more windows");
|
|
83
|
+
return out.join("\n");
|
|
84
|
+
}
|
|
46
85
|
export function formatCodemodeCall(code, theme) {
|
|
47
86
|
const preview = code.trim().replace(/\s+/g, " ").slice(0, 80);
|
|
48
87
|
return header(theme, "codemode", [`${preview}${code.trim().length > 80 ? "…" : ""}`]);
|
|
@@ -61,6 +100,20 @@ export function formatSearchResult(response, meta, theme) {
|
|
|
61
100
|
const label = hitLabel(hit);
|
|
62
101
|
return paint(theme, "toolOutput", label ? ` ${loc} ${label}` : ` ${loc}`);
|
|
63
102
|
});
|
|
103
|
+
if (hits.length === 0) {
|
|
104
|
+
rows.push(paint(theme, "muted", " 0 hits"));
|
|
105
|
+
const next = Array.isArray(response.suggested_next)
|
|
106
|
+
? response.suggested_next.filter((item) => typeof item === "string")
|
|
107
|
+
: [];
|
|
108
|
+
if (next.length > 0) {
|
|
109
|
+
for (const query of next.slice(0, 4)) {
|
|
110
|
+
rows.push(paint(theme, "toolOutput", ` try ${query}`));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
rows.push(paint(theme, "toolOutput", " try asgrep.find(query) or asgrep.defs(symbol) or asgrep.search(query, { in: \"src\" })"));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
64
117
|
if (hits.length > 24) {
|
|
65
118
|
rows.push(paint(theme, "muted", ` … ${hits.length - 24} more`));
|
|
66
119
|
}
|
|
@@ -173,7 +226,10 @@ export function truncateToWidth(text, maxWidth, ellipsis = "...") {
|
|
|
173
226
|
width += cell.width;
|
|
174
227
|
index += cell.length;
|
|
175
228
|
}
|
|
176
|
-
|
|
229
|
+
// The cut can land inside a painted span — its closing SGR is past the
|
|
230
|
+
// budget, so the ellipsis and everything after would inherit the open color.
|
|
231
|
+
// Close it explicitly; harmless when the kept spans were already balanced.
|
|
232
|
+
return kept + (kept.includes("\u001b[") ? "\u001b[0m" : "") + ellipsis;
|
|
177
233
|
}
|
|
178
234
|
function compactValue(value) {
|
|
179
235
|
if (value === null || value === undefined)
|
|
@@ -221,6 +277,9 @@ export function formatCodemodeResult(value, meta = {}, theme) {
|
|
|
221
277
|
if (meta.wallMs !== undefined)
|
|
222
278
|
bits.push(`${meta.wallMs}ms`);
|
|
223
279
|
const title = header(theme, "codemode", bits);
|
|
280
|
+
if (value === undefined) {
|
|
281
|
+
return `${title}\n${paint(theme, "muted", " (no return statement; add `return` to send a value to the model)")}`;
|
|
282
|
+
}
|
|
224
283
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
225
284
|
const rows = Object.entries(value).slice(0, 16).map(([key, entry]) => paint(theme, "toolOutput", ` ${key}: ${compactValue(entry)}`));
|
|
226
285
|
return [title, ...rows].join("\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ast-sgrep",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Native Code Mode, structural, graph, and semantic code search for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"import": "./dist/index.js"
|
|
37
37
|
},
|
|
38
38
|
"./runtime": {
|
|
39
|
-
"types": "./dist/runtime.d.ts",
|
|
40
|
-
"import": "./dist/runtime.js"
|
|
39
|
+
"types": "./dist/runtime/runtime.d.ts",
|
|
40
|
+
"import": "./dist/runtime/runtime.js"
|
|
41
41
|
},
|
|
42
42
|
"./code-mode": {
|
|
43
43
|
"types": "./dist/code-mode.d.ts",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"image": "./assets/preview.png"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
|
-
"build": "tsc -p tsconfig.json",
|
|
54
|
+
"build": "tsc -p tsconfig.json && node scripts/copy-guest-worker.mjs",
|
|
55
55
|
"build:native": "cargo build -p ast-sgrep-codemode-napi --release && node ./scripts/copy-native.mjs",
|
|
56
|
-
"test": "ASGREP_CODEMODE_BACKEND=cli node --import tsx --test ../../../tests/pi/extension/code-mode.test.ts ../../../tests/pi/extension/codemode.test.ts ../../../tests/pi/extension/commands.test.ts ../../../tests/pi/extension/present.test.ts ../../../tests/pi/extension/runtime.test.ts ../../../tests/pi/extension/security.test.ts ../../../tests/pi/extension/session-pool.test.ts ../../../tests/pi/extension/skill-workflow.test.ts ../../../tests/pi/extension/tools.test.ts",
|
|
56
|
+
"test": "ASGREP_CODEMODE_BACKEND=cli node --import tsx --test ../../../tests/pi/extension/code-mode.test.ts ../../../tests/pi/extension/codemode.test.ts ../../../tests/pi/extension/commands.test.ts ../../../tests/pi/extension/present.test.ts ../../../tests/pi/extension/runtime.test.ts ../../../tests/pi/extension/security.test.ts ../../../tests/pi/extension/session-pool.test.ts ../../../tests/pi/extension/skill-workflow.test.ts ../../../tests/pi/extension/sqlite.test.ts ../../../tests/pi/extension/tools.test.ts",
|
|
57
57
|
"test:native": "node --import tsx --test ../../../tests/pi/extension/native-inprocess.test.ts",
|
|
58
58
|
"test:all": "npm test && npm run test:native",
|
|
59
59
|
"prepack": "npm run build"
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"node": ">=22.19.0"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"ast-sgrep": "2.0.0",
|
|
65
|
+
"ast-sgrep": ">=2.0.0 <3",
|
|
66
66
|
"typebox": "^1.0.0"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|