pi-ast-sgrep 2.0.2 → 2.2.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 +16 -19
- package/dist/code-mode.d.ts +1 -1
- package/dist/code-mode.js +1 -1
- package/dist/codemode/connector.d.ts +18 -3
- package/dist/codemode/connector.js +85 -31
- package/dist/codemode/dispatch.d.ts +13 -1
- package/dist/codemode/dispatch.js +87 -24
- 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/native.d.ts +1 -1
- package/dist/codemode/native.js +1 -1
- 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 +42 -2
- package/dist/codemode/types.js +40 -15
- 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 +123 -0
- package/dist/host/results.js +126 -0
- package/dist/host/tools.d.ts +28 -0
- package/dist/host/tools.js +802 -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 +446 -0
- package/dist/runtime/index-health.d.ts +16 -0
- package/dist/runtime/index-health.js +111 -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 +66 -0
- package/dist/ui/card.js +375 -0
- package/dist/ui/present.d.ts +89 -0
- package/dist/ui/present.js +391 -0
- package/package.json +8 -7
- package/dist/codemode/sandbox-worker.d.ts +0 -1
- package/dist/codemode/sandbox-worker.js +0 -204
- package/dist/present.d.ts +0 -70
- package/dist/present.js +0 -260
- package/dist/runtime.d.ts +0 -137
- package/dist/runtime.js +0 -799
package/dist/ui/card.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
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 { displayWidth, hitLabel, hitLocation, paint, sanitizeContent, summarizeValue, 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
|
+
/** Rounded frame glyphs — pi themes may provide theme.boxRound; default ASCII-art set. */
|
|
26
|
+
const BOX = { tl: "\u256d", tr: "\u256e", bl: "\u2570", br: "\u256f", h: "\u2500", v: "\u2502" };
|
|
27
|
+
function boxOf(theme) {
|
|
28
|
+
const b = theme?.boxRound;
|
|
29
|
+
if (b && typeof b.topLeft === "string" && typeof b.horizontal === "string" && typeof b.vertical === "string") {
|
|
30
|
+
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 };
|
|
31
|
+
}
|
|
32
|
+
return BOX;
|
|
33
|
+
}
|
|
34
|
+
function borderKey(model) {
|
|
35
|
+
if (model.error)
|
|
36
|
+
return "error";
|
|
37
|
+
if (model.running)
|
|
38
|
+
return "accent";
|
|
39
|
+
return "dim";
|
|
40
|
+
}
|
|
41
|
+
/** Tool box background per state — the same keys pi themes tint native tool rows with. */
|
|
42
|
+
function backgroundKey(model) {
|
|
43
|
+
if (model.error)
|
|
44
|
+
return "toolErrorBg";
|
|
45
|
+
if (model.running)
|
|
46
|
+
return "toolPendingBg";
|
|
47
|
+
return "toolSuccessBg";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Paint the tool box background across the full row, when the theme has one.
|
|
51
|
+
* Without this the host's message background shows through the card, which
|
|
52
|
+
* reads as a dark band beside the border.
|
|
53
|
+
*/
|
|
54
|
+
function backgroundPaint(theme, model) {
|
|
55
|
+
const bg = theme?.bg;
|
|
56
|
+
if (typeof bg !== "function")
|
|
57
|
+
return undefined;
|
|
58
|
+
try {
|
|
59
|
+
const key = backgroundKey(model);
|
|
60
|
+
if (typeof bg.call(theme, key, "x") !== "string")
|
|
61
|
+
return undefined;
|
|
62
|
+
return (text) => {
|
|
63
|
+
const painted = bg.call(theme, key, text);
|
|
64
|
+
return typeof painted === "string" ? painted : text;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Top/bottom bar with an optional label embedded in the rule. Geometry is in
|
|
72
|
+
* display cells (displayWidth): \u256d + 3 rules on the left, corner on the right. */
|
|
73
|
+
function frameBar(theme, box, border, left, right, label, width) {
|
|
74
|
+
const leftRaw = left + box.h.repeat(3);
|
|
75
|
+
const shown = label ? clamp(" " + label + " ", Math.max(0, width - displayWidth(leftRaw) - 1)) : "";
|
|
76
|
+
const fill = Math.max(0, width - displayWidth(leftRaw) - displayWidth(shown) - 1);
|
|
77
|
+
return border(leftRaw) + shown + border(box.h.repeat(fill)) + border(right);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Cut a row to `width` display columns (pi's cell scale: tab = 3, wide = 2).
|
|
81
|
+
*
|
|
82
|
+
* Delegates to the shared cell-aware truncator so every surface measures with
|
|
83
|
+
* one rule. Counting code units here instead (the previous shape) under-counted
|
|
84
|
+
* tabs threefold and made tab-indented code rows overflow the card — the crash
|
|
85
|
+
* pi reports as "Rendered line N exceeds terminal width".
|
|
86
|
+
*/
|
|
87
|
+
function clamp(text, width) {
|
|
88
|
+
return truncateToWidth(text, Math.max(1, width), "\u2026");
|
|
89
|
+
}
|
|
90
|
+
function fitPath(text, budget) {
|
|
91
|
+
if (visibleWidth(text) <= budget)
|
|
92
|
+
return text;
|
|
93
|
+
if (budget <= 1)
|
|
94
|
+
return "\u2026";
|
|
95
|
+
return "\u2026" + text.slice(Math.max(0, text.length - budget + 1));
|
|
96
|
+
}
|
|
97
|
+
function fmtMs(ms) {
|
|
98
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
99
|
+
return "";
|
|
100
|
+
return ms < 1000 ? Math.round(ms) + "ms" : (ms / 1000).toFixed(1) + "s";
|
|
101
|
+
}
|
|
102
|
+
/** "✓ search 12ms \"query\"" — one aligned op row. */
|
|
103
|
+
function opRow(theme, op, width) {
|
|
104
|
+
const marker = op.ok ? paint(theme, "success", "\u2713") : paint(theme, "error", "\u00d7");
|
|
105
|
+
const tool = paint(theme, "syntaxFunction", op.tool.slice(0, TOOL_COL).padEnd(TOOL_COL));
|
|
106
|
+
const dur = paint(theme, "dim", fmtMs(op.ms).padStart(DUR_COL));
|
|
107
|
+
const target = op.target ? " " + paint(theme, "muted", fitPath(op.target, Math.max(1, width - TOOL_COL - DUR_COL - 9))) : "";
|
|
108
|
+
return " " + marker + " " + tool + " " + dur + target;
|
|
109
|
+
}
|
|
110
|
+
/** " 1. path:line symbol · kind" — one numbered hit row. */
|
|
111
|
+
function hitRow(theme, n, hit, width) {
|
|
112
|
+
const num = paint(theme, "dim", String(n).padStart(2) + ".");
|
|
113
|
+
const loc = hitLocation(hit);
|
|
114
|
+
const locBudget = Math.max(8, Math.floor(width * 0.62));
|
|
115
|
+
const locText = paint(theme, "accent", fitPath(loc, locBudget));
|
|
116
|
+
const label = hitLabel(hit);
|
|
117
|
+
const row = " " + num + " " + locText + (label ? " " + paint(theme, "muted", label) : "");
|
|
118
|
+
return clamp(row, width);
|
|
119
|
+
}
|
|
120
|
+
export class AsgrepCard {
|
|
121
|
+
theme;
|
|
122
|
+
model;
|
|
123
|
+
cache;
|
|
124
|
+
set(theme, model) {
|
|
125
|
+
this.theme = theme;
|
|
126
|
+
this.model = model;
|
|
127
|
+
this.cache = undefined;
|
|
128
|
+
}
|
|
129
|
+
invalidate() {
|
|
130
|
+
this.cache = undefined;
|
|
131
|
+
}
|
|
132
|
+
render(width = 80) {
|
|
133
|
+
const theme = this.theme;
|
|
134
|
+
const model = this.model;
|
|
135
|
+
if (!model || width <= 0)
|
|
136
|
+
return [];
|
|
137
|
+
if (this.cache?.width === width)
|
|
138
|
+
return this.cache.lines;
|
|
139
|
+
// Pi hands us the full terminal width and renders this card with
|
|
140
|
+
// renderShell "self", so the frame must span every column the host gave
|
|
141
|
+
// us. Capping it left the host's message background showing as a dark
|
|
142
|
+
// band to the right of the border.
|
|
143
|
+
const lines = framedLines(theme, model, Math.max(8, width));
|
|
144
|
+
this.cache = { width, lines };
|
|
145
|
+
return lines;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Rounded card: header embedded in the top rule, body rows in \u2502 gutters. */
|
|
149
|
+
function framedLines(theme, model, width) {
|
|
150
|
+
const box = boxOf(theme);
|
|
151
|
+
const key = borderKey(model);
|
|
152
|
+
const border = (text) => paint(theme, key, text);
|
|
153
|
+
const inner = Math.max(1, width - 4); // "\u2502 " + content + " \u2502"
|
|
154
|
+
const label = frameLabel(model);
|
|
155
|
+
const rows = bodyLines(theme, model, inner);
|
|
156
|
+
const background = backgroundPaint(theme, model);
|
|
157
|
+
const fill = (line) => (background ? background(line) : line);
|
|
158
|
+
const out = [fill(frameBar(theme, box, border, box.tl, box.tr, label, width))];
|
|
159
|
+
for (const row of rows) {
|
|
160
|
+
const body = clamp(row, inner);
|
|
161
|
+
const pad = Math.max(0, inner - displayWidth(body));
|
|
162
|
+
out.push(fill(border(box.v) + " " + body + " ".repeat(pad) + " " + border(box.v)));
|
|
163
|
+
}
|
|
164
|
+
out.push(fill(frameBar(theme, box, border, box.bl, box.br, null, width)));
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
function bodyLines(theme, model, width) {
|
|
168
|
+
const lines = [];
|
|
169
|
+
const maxOps = model.expanded ? 24 : 8;
|
|
170
|
+
const maxHits = model.expanded ? 24 : 12;
|
|
171
|
+
const ops = (model.ops ?? []).slice(0, maxOps);
|
|
172
|
+
for (const op of ops)
|
|
173
|
+
lines.push(clamp(opRow(theme, op, width), width));
|
|
174
|
+
if ((model.ops?.length ?? 0) > ops.length) {
|
|
175
|
+
lines.push(paint(theme, "dim", " \u2026 " + ((model.ops?.length ?? 0) - ops.length) + " more calls"));
|
|
176
|
+
}
|
|
177
|
+
const hits = (model.hits ?? []).slice(0, maxHits);
|
|
178
|
+
hits.forEach((hit, i) => lines.push(hitRow(theme, i + 1, hit, width)));
|
|
179
|
+
if ((model.hits?.length ?? 0) > hits.length) {
|
|
180
|
+
lines.push(paint(theme, "dim", " \u2026 " + ((model.hits?.length ?? 0) - hits.length) + " more"));
|
|
181
|
+
}
|
|
182
|
+
for (const edit of (model.edits ?? []).slice(0, model.expanded ? 12 : 4)) {
|
|
183
|
+
const head = " " + paint(theme, "accent", sanitizeContent(edit.path ?? "?")) + (edit.line ? paint(theme, "dim", ":" + edit.line) : "");
|
|
184
|
+
lines.push(clamp(head, width));
|
|
185
|
+
for (const line of (edit.removed ?? []).slice(0, model.expanded ? 24 : 8)) {
|
|
186
|
+
lines.push(" " + paint(theme, "error", "- " + clamp(line, width - 5)));
|
|
187
|
+
}
|
|
188
|
+
for (const line of (edit.added ?? []).slice(0, model.expanded ? 24 : 8)) {
|
|
189
|
+
lines.push(" " + paint(theme, "success", "+ " + clamp(line, width - 5)));
|
|
190
|
+
}
|
|
191
|
+
if (edit.truncated)
|
|
192
|
+
lines.push(paint(theme, "dim", " \u2026"));
|
|
193
|
+
}
|
|
194
|
+
if (model.error) {
|
|
195
|
+
for (const line of model.error.split("\n").slice(0, model.expanded ? 12 : 4)) {
|
|
196
|
+
lines.push(" " + paint(theme, "error", clamp(line, width - 1)));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else if (model.resultLines && model.resultLines.length > 0) {
|
|
200
|
+
const shown = model.expanded ? model.resultLines : model.resultLines.slice(0, 8);
|
|
201
|
+
for (const line of shown)
|
|
202
|
+
lines.push(" " + paint(theme, "muted", clamp(line, width - 1)));
|
|
203
|
+
if (!model.expanded && model.resultLines.length > shown.length) {
|
|
204
|
+
lines.push(paint(theme, "dim", " \u2026 " + (model.resultLines.length - shown.length) + " more result lines"));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const note of model.notes ?? []) {
|
|
208
|
+
lines.push(" " + paint(theme, "warning", "! " + clamp(note, width - 3)));
|
|
209
|
+
}
|
|
210
|
+
return lines;
|
|
211
|
+
}
|
|
212
|
+
/** Target bits taken from the call arguments, so a running card can name what
|
|
213
|
+
* the call is about before any result exists. Never duplicated on completion:
|
|
214
|
+
* once details land they own the label. */
|
|
215
|
+
function callTargetBits(args) {
|
|
216
|
+
if (!args)
|
|
217
|
+
return [];
|
|
218
|
+
const bits = [];
|
|
219
|
+
if (typeof args.query === "string" && args.query)
|
|
220
|
+
bits.push(JSON.stringify(sanitizeContent(args.query)));
|
|
221
|
+
else if (typeof args.symbol === "string" && args.symbol)
|
|
222
|
+
bits.push(sanitizeContent(args.symbol));
|
|
223
|
+
else if (typeof args.code === "string" && args.code)
|
|
224
|
+
bits.push(sanitizeContent(args.code.trim().replace(/\s+/gu, " ")).slice(0, 60));
|
|
225
|
+
if (typeof args.path === "string" && args.path)
|
|
226
|
+
bits.push(sanitizeContent(args.path));
|
|
227
|
+
else if (typeof args.ref === "string" && args.ref)
|
|
228
|
+
bits.push(sanitizeContent(args.ref));
|
|
229
|
+
return bits;
|
|
230
|
+
}
|
|
231
|
+
function editsOf(value) {
|
|
232
|
+
if (value && typeof value === "object" && Array.isArray(value.edits)) {
|
|
233
|
+
return value.edits
|
|
234
|
+
.filter((e) => !!e && typeof e === "object")
|
|
235
|
+
.filter((e) => Array.isArray(e.removed) || Array.isArray(e.added))
|
|
236
|
+
.map((e) => {
|
|
237
|
+
const entry = {
|
|
238
|
+
truncated: e.truncated === true,
|
|
239
|
+
};
|
|
240
|
+
if (typeof e.path === "string")
|
|
241
|
+
entry.path = sanitizeContent(e.path);
|
|
242
|
+
if (typeof e.line === "number")
|
|
243
|
+
entry.line = e.line;
|
|
244
|
+
if (Array.isArray(e.removed))
|
|
245
|
+
entry.removed = e.removed.map((line) => sanitizeContent(String(line)));
|
|
246
|
+
if (Array.isArray(e.added))
|
|
247
|
+
entry.added = e.added.map((line) => sanitizeContent(String(line)));
|
|
248
|
+
return entry;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
function hitsOf(value) {
|
|
254
|
+
if (value && typeof value === "object" && Array.isArray(value.hits)) {
|
|
255
|
+
return value.hits;
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
function resultPreviewLines(value) {
|
|
260
|
+
if (value === undefined || value === null)
|
|
261
|
+
return undefined;
|
|
262
|
+
if (typeof value === "string")
|
|
263
|
+
return value.split("\n").filter((line) => line.length > 0).slice(0, 16).map(sanitizeContent);
|
|
264
|
+
if (Array.isArray(value))
|
|
265
|
+
return [value.length + " value" + (value.length === 1 ? "" : "s")];
|
|
266
|
+
if (typeof value === "object") {
|
|
267
|
+
// Shaped summary only: transport fields (tool/command/schema_version/ok)
|
|
268
|
+
// describe the wire envelope, not the answer, and a raw JSON dump of them
|
|
269
|
+
// is noise in the transcript.
|
|
270
|
+
const rows = summarizeValue(value);
|
|
271
|
+
return rows.length > 0 ? rows : undefined;
|
|
272
|
+
}
|
|
273
|
+
return [sanitizeContent(String(value))];
|
|
274
|
+
}
|
|
275
|
+
/** Build the card model from the tool result's details payload. */
|
|
276
|
+
export function cardModel(result, options, callArgs) {
|
|
277
|
+
const details = (result.details && typeof result.details === "object" ? result.details : {});
|
|
278
|
+
const command = typeof details.command === "string" ? details.command : "asgrep";
|
|
279
|
+
const expanded = options.expanded === true;
|
|
280
|
+
// In-flight partial updates carry only {command, phase}.
|
|
281
|
+
if (options.isPartial && !("ok" in details)) {
|
|
282
|
+
const phase = typeof details.phase === "string" && details.phase !== "started" ? details.phase : undefined;
|
|
283
|
+
return {
|
|
284
|
+
command,
|
|
285
|
+
title: [...callTargetBits(callArgs), phase],
|
|
286
|
+
running: true,
|
|
287
|
+
expanded,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const title = [];
|
|
291
|
+
if (typeof details.query === "string" && details.query)
|
|
292
|
+
title.push(JSON.stringify(sanitizeContent(details.query)));
|
|
293
|
+
if (typeof details.mode === "string")
|
|
294
|
+
title.push(sanitizeContent(details.mode));
|
|
295
|
+
const error = details.error;
|
|
296
|
+
if (result.isError || details.ok === false || error) {
|
|
297
|
+
return {
|
|
298
|
+
command,
|
|
299
|
+
title,
|
|
300
|
+
error: typeof error?.message === "string" ? sanitizeContent(error.message) : "tool failed",
|
|
301
|
+
expanded,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const response = details.response;
|
|
305
|
+
if (command === "status" || command === "index" || command === "reindex") {
|
|
306
|
+
const state = typeof response?.status === "string" ? response.status
|
|
307
|
+
: typeof response?.index_status === "string" ? response.index_status
|
|
308
|
+
: response?.ok === true ? "ok" : undefined;
|
|
309
|
+
if (state)
|
|
310
|
+
title.push(state);
|
|
311
|
+
const counts = response?.counts;
|
|
312
|
+
if (counts && typeof counts === "object") {
|
|
313
|
+
title.push(Object.entries(counts).slice(0, 4).map(([k, v]) => k + "=" + String(v)).join(" "));
|
|
314
|
+
}
|
|
315
|
+
else if (response) {
|
|
316
|
+
// Flat status envelope: file_count/symbol_count/caller_count + embed info.
|
|
317
|
+
const flat = [];
|
|
318
|
+
for (const k of ["file_count", "symbol_count", "caller_count"]) {
|
|
319
|
+
const v = response[k];
|
|
320
|
+
if (typeof v === "number" || typeof v === "bigint")
|
|
321
|
+
flat.push(k.replace(/_count$/, "s") + "=" + String(v));
|
|
322
|
+
}
|
|
323
|
+
if (typeof response.embed_backend === "string")
|
|
324
|
+
flat.push(response.embed_backend);
|
|
325
|
+
if (flat.length > 0)
|
|
326
|
+
title.push(flat.join(" "));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const ms = typeof details.wallMs === "number" ? details.wallMs : typeof details.activationMs === "number" ? details.activationMs : undefined;
|
|
330
|
+
if (ms !== undefined)
|
|
331
|
+
title.push(fmtMs(ms));
|
|
332
|
+
if (typeof details.backend === "string")
|
|
333
|
+
title.push(details.backend);
|
|
334
|
+
const trace = Array.isArray(details.trace) ? details.trace : undefined;
|
|
335
|
+
const ops = trace?.map((t) => ({ tool: t.tool, target: t.target ?? "", ok: t.ok !== false, ms: typeof t.ms === "number" ? t.ms : 0 }));
|
|
336
|
+
const hits = hitsOf(response) ?? hitsOf(details.result);
|
|
337
|
+
const resultEdits = editsOf(details.result) ?? editsOf(response);
|
|
338
|
+
// read envelopes carry windows: preview each window's first lines.
|
|
339
|
+
const windows = command === "read" && response && Array.isArray(response.windows)
|
|
340
|
+
? response.windows
|
|
341
|
+
: undefined;
|
|
342
|
+
const readLines = windows?.flatMap((w) => [
|
|
343
|
+
sanitizeContent((w.path ?? "?") + ":" + (w.start ?? 1) + "-" + (w.end ?? "")),
|
|
344
|
+
...(typeof w.text === "string" ? sanitizeContent(w.text).split("\n").slice(0, expanded ? 20 : 6).map((l) => " " + l) : []),
|
|
345
|
+
]);
|
|
346
|
+
// When edits carry diffs they are the interesting part of the result.
|
|
347
|
+
const resultLines = hits || resultEdits ? undefined : (readLines ?? resultPreviewLines(details.result));
|
|
348
|
+
const model = { command, title, expanded };
|
|
349
|
+
if (ops && ops.length > 0)
|
|
350
|
+
model.ops = ops;
|
|
351
|
+
if (hits)
|
|
352
|
+
model.hits = hits;
|
|
353
|
+
if (resultEdits)
|
|
354
|
+
model.edits = resultEdits;
|
|
355
|
+
if (resultLines)
|
|
356
|
+
model.resultLines = resultLines;
|
|
357
|
+
// Warnings travel with the model-visible text; keep them visible in the TUI
|
|
358
|
+
// too, or the transcript looks clean while the answer is qualified.
|
|
359
|
+
if (Array.isArray(details.notes)) {
|
|
360
|
+
const notes = details.notes.filter((note) => typeof note === "string" && note.length > 0);
|
|
361
|
+
if (notes.length > 0)
|
|
362
|
+
model.notes = notes.map((note) => sanitizeContent(note));
|
|
363
|
+
}
|
|
364
|
+
return model;
|
|
365
|
+
}
|
|
366
|
+
/** renderResult entrypoint: bind one card per result slot, feed it details. */
|
|
367
|
+
export function renderAsgrepResult(result, options, theme, context) {
|
|
368
|
+
const prev = context?.lastComponent;
|
|
369
|
+
const card = prev instanceof AsgrepCard ? prev : new AsgrepCard();
|
|
370
|
+
const args = context?.args;
|
|
371
|
+
card.set(theme, cardModel(result, options, args && !Array.isArray(args) ? args : undefined));
|
|
372
|
+
if (context)
|
|
373
|
+
context.lastComponent = card;
|
|
374
|
+
return card;
|
|
375
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Neat asgrep tool chrome for the Pi TUI and the model-visible content. */
|
|
2
|
+
export type PresentTheme = {
|
|
3
|
+
bold(text: string): string;
|
|
4
|
+
fg(role: string, text: string): string;
|
|
5
|
+
/** Optional: pi themes expose tool box backgrounds (toolSuccessBg/toolErrorBg/toolPendingBg). */
|
|
6
|
+
bg?(role: string, text: string): string;
|
|
7
|
+
};
|
|
8
|
+
export type HitLike = {
|
|
9
|
+
file?: unknown;
|
|
10
|
+
path?: unknown;
|
|
11
|
+
ref?: unknown;
|
|
12
|
+
symbol?: unknown;
|
|
13
|
+
kind?: unknown;
|
|
14
|
+
preview?: unknown;
|
|
15
|
+
start_line?: unknown;
|
|
16
|
+
line?: unknown;
|
|
17
|
+
lines?: unknown;
|
|
18
|
+
};
|
|
19
|
+
export type EnvelopeLike = {
|
|
20
|
+
ok?: unknown;
|
|
21
|
+
hits?: unknown;
|
|
22
|
+
count?: unknown;
|
|
23
|
+
total?: unknown;
|
|
24
|
+
status?: unknown;
|
|
25
|
+
index_status?: unknown;
|
|
26
|
+
counts?: unknown;
|
|
27
|
+
backend?: unknown;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
};
|
|
30
|
+
export declare const ASGREP_PROMPT_SNIPPET = "Code search by intent, symbol, defs, callers, pattern (asgrep; use without being asked)";
|
|
31
|
+
export declare const ASGREP_PROMPT_GUIDELINES: readonly ["Any code lookup (function, def, caller, intent, pattern): call asgrep first.", "Compose in Code Mode (search/defs/read with Promise.all, return a small shaped value); grep only for exact strings or filenames. Bound with in: \"path\"; on 0 hits use suggested_next."];
|
|
32
|
+
export declare function paint(theme: PresentTheme | undefined, role: string, text: string, bold?: boolean): string;
|
|
33
|
+
export declare function hitLocation(hit: HitLike): string;
|
|
34
|
+
export declare function hitLabel(hit: HitLike): string;
|
|
35
|
+
export declare function formatEditResult(response: EnvelopeLike, theme?: PresentTheme): string;
|
|
36
|
+
/** Model-visible text for a read envelope: the window contents themselves. */
|
|
37
|
+
export declare function formatReadResult(response: EnvelopeLike, theme?: PresentTheme): string;
|
|
38
|
+
/**
|
|
39
|
+
* Model-facing result text is deliberately lean: the tool call already carries
|
|
40
|
+
* the query/mode, the TUI card renders timing and backend for the human, and
|
|
41
|
+
* every token here is re-sent with the whole transcript. The first line is the
|
|
42
|
+
* only chrome: "<command>: <payload summary>".
|
|
43
|
+
*/
|
|
44
|
+
export declare function formatSearchResult(response: EnvelopeLike, meta: {
|
|
45
|
+
command: string;
|
|
46
|
+
excerptLines?: number;
|
|
47
|
+
}, theme?: PresentTheme): string;
|
|
48
|
+
export declare function formatStatusResult(response: EnvelopeLike, theme?: PresentTheme): string;
|
|
49
|
+
export declare function formatIndexResult(command: string, response: EnvelopeLike, theme?: PresentTheme): string;
|
|
50
|
+
/**
|
|
51
|
+
* Strip terminal control sequences and C0/C1 controls from untrusted content
|
|
52
|
+
* (tool output, code windows, paths) before this extension paints it. Without
|
|
53
|
+
* this a stray ESC in a file would sit inside our own SGR span, leaving an
|
|
54
|
+
* unterminated color and mis-measuring the row's width.
|
|
55
|
+
*/
|
|
56
|
+
export declare function sanitizeContent(text: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Conservative display width on pi's scale: never under-counts what pi measures.
|
|
59
|
+
*
|
|
60
|
+
* Pi expands tabs to three spaces and measures grapheme clusters with East Asian
|
|
61
|
+
* Width (CJK/fullwidth/emoji = 2); lone combining marks, variation selectors and
|
|
62
|
+
* joiners are zero there. Re-deriving that table here would be a second source of
|
|
63
|
+
* truth that can drift, so this counts only what the card itself draws exactly
|
|
64
|
+
* (one cell) and rounds every other non-ASCII code point UP to two. A line that
|
|
65
|
+
* measures `width` here is therefore at most `width` in the terminal: over-counting
|
|
66
|
+
* can only leave slack before the right border, while under-counting is what pi
|
|
67
|
+
* kills the process for ("Rendered line N exceeds terminal width").
|
|
68
|
+
*/
|
|
69
|
+
export declare function displayWidth(text: string): number;
|
|
70
|
+
/** Local stand-in so we do not take a pi-tui dependency. Over-counts wide glyphs rather than under-count. */
|
|
71
|
+
export declare function visibleWidth(text: string): number;
|
|
72
|
+
export declare function truncateToWidth(text: string, maxWidth: number, ellipsis?: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* One summary line per interesting entry of a shaped value: known shapes get a
|
|
75
|
+
* sentence ("3 windows · path:1-340"), everything else `key: value` with the
|
|
76
|
+
* value compacted. Transport fields are dropped rather than rendered.
|
|
77
|
+
*/
|
|
78
|
+
export declare function summarizeValue(value: unknown, limit?: number): string[];
|
|
79
|
+
export declare function formatCodemodeResult(value: unknown, meta?: {
|
|
80
|
+
stats?: {
|
|
81
|
+
calls: number;
|
|
82
|
+
batchedCalls: number;
|
|
83
|
+
parallelSpawnCalls: number;
|
|
84
|
+
stickyCalls?: number;
|
|
85
|
+
waves: number;
|
|
86
|
+
};
|
|
87
|
+
wallMs?: number;
|
|
88
|
+
backend?: string;
|
|
89
|
+
}, theme?: PresentTheme): string;
|