pi-ast-sgrep 2.1.1 → 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 +1 -0
- package/dist/codemode/connector.d.ts +1 -0
- package/dist/codemode/connector.js +32 -6
- package/dist/codemode/dispatch.js +4 -1
- package/dist/codemode/native.d.ts +1 -1
- package/dist/codemode/native.js +1 -1
- package/dist/codemode/types.d.ts +9 -0
- package/dist/codemode/types.js +12 -13
- package/dist/host/results.d.ts +8 -0
- package/dist/host/results.js +47 -1
- package/dist/host/tools.d.ts +19 -0
- package/dist/host/tools.js +319 -127
- package/dist/runtime/freshness.js +40 -7
- package/dist/runtime/index-health.d.ts +1 -1
- package/dist/runtime/index-health.js +4 -2
- package/dist/runtime/types.d.ts +1 -1
- package/dist/runtime/types.js +1 -1
- package/dist/ui/card.d.ts +4 -1
- package/dist/ui/card.js +125 -66
- package/dist/ui/present.d.ts +37 -36
- package/dist/ui/present.js +226 -154
- package/package.json +5 -4
|
@@ -18,7 +18,7 @@ async function probeIndexHealth(runtime, rootContext, options) {
|
|
|
18
18
|
const status = runtime.nativeCall
|
|
19
19
|
? await runtime.nativeCall("index_status", {}, rootContext, options)
|
|
20
20
|
: await runtime.run(["status", ".", "--json"], rootContext, options);
|
|
21
|
-
return indexHealth(status
|
|
21
|
+
return indexHealth(status);
|
|
22
22
|
}
|
|
23
23
|
catch (cause) {
|
|
24
24
|
if (!incompatibleStatusFailure(cause))
|
|
@@ -26,11 +26,18 @@ async function probeIndexHealth(runtime, rootContext, options) {
|
|
|
26
26
|
return "incompatible";
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Run index_repo via the host's native call (the extension routes it out of
|
|
31
|
+
* process — see host/tools.ts) or CLI argv. force=true → reindex.
|
|
32
|
+
*
|
|
33
|
+
* Implicit (freshness-driven) refreshes index lexical/AST rows only: neural
|
|
34
|
+
* embeddings for a cold repo took 36-60s on large trees before the first search
|
|
35
|
+
* could answer. Embeddings are built by the explicit index/reindex tool.
|
|
36
|
+
*/
|
|
30
37
|
async function runIndex(runtime, force, rootContext, options) {
|
|
31
38
|
const response = runtime.nativeCall
|
|
32
|
-
? await runtime.nativeCall("index_repo", { force }, rootContext, options)
|
|
33
|
-
: await runtime.run([force ? "reindex" : "index", ".", "--json"], rootContext, options);
|
|
39
|
+
? await runtime.nativeCall("index_repo", { force, use_embed: false }, rootContext, options)
|
|
40
|
+
: await runtime.run([force ? "reindex" : "index", ".", "--json", "--no-embed"], rootContext, options);
|
|
34
41
|
const { failed, walkErrors } = indexCompletion(response, true);
|
|
35
42
|
if (failed > 0 || walkErrors) {
|
|
36
43
|
throw new RuntimeError("INDEX_UPDATE_INCOMPLETE", "ast-sgrep did not complete the full index reconciliation", { failed, walkErrors, force });
|
|
@@ -41,8 +48,8 @@ async function runTargetedIndex(runtime, paths, rootContext, options) {
|
|
|
41
48
|
for (let offset = 0; offset < paths.length; offset += MAX_TARGETED_INDEX_PATHS) {
|
|
42
49
|
const chunk = paths.slice(offset, offset + MAX_TARGETED_INDEX_PATHS);
|
|
43
50
|
const response = runtime.nativeCall
|
|
44
|
-
? await runtime.nativeCall("index_repo", { paths: chunk }, rootContext, options)
|
|
45
|
-
: await runtime.run(["index", ".", "--json", ...chunk.flatMap((path) => ["--path", path])], rootContext, options);
|
|
51
|
+
? await runtime.nativeCall("index_repo", { paths: chunk, use_embed: false }, rootContext, options)
|
|
52
|
+
: await runtime.run(["index", ".", "--json", "--no-embed", ...chunk.flatMap((path) => ["--path", path])], rootContext, options);
|
|
46
53
|
const { failed } = indexCompletion(response, false);
|
|
47
54
|
if (failed > 0) {
|
|
48
55
|
throw new RuntimeError("INDEX_UPDATE_INCOMPLETE", `ast-sgrep failed to update ${failed} changed path${failed === 1 ? "" : "s"}`, { failed, pathCount: chunk.length });
|
|
@@ -133,6 +140,22 @@ function markStateFullScan(state) {
|
|
|
133
140
|
function cancelledRefreshWait() {
|
|
134
141
|
return new RuntimeError("CANCELLED", "ast-sgrep freshness wait was cancelled");
|
|
135
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* A cancellation that belongs to another caller's dead refresh, not to this
|
|
145
|
+
* caller. The last waiter's cancel aborts shared work (resource hygiene); a
|
|
146
|
+
* caller holding a live signal must never inherit that teardown as its own
|
|
147
|
+
* failure — it settles the dead refresh and owns a fresh one instead.
|
|
148
|
+
*/
|
|
149
|
+
function isForeignRefreshCancel(cause, signal) {
|
|
150
|
+
if (signal?.aborted === true)
|
|
151
|
+
return false;
|
|
152
|
+
if (cause instanceof RuntimeError)
|
|
153
|
+
return cause.code === "CANCELLED";
|
|
154
|
+
if (cause instanceof Error && cause.name === "AbortError")
|
|
155
|
+
return true;
|
|
156
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
157
|
+
return /aborted|was cancelled/i.test(message);
|
|
158
|
+
}
|
|
136
159
|
/** Stop one caller waiting without transferring cancellation ownership to shared work. */
|
|
137
160
|
function waitForRefresh(refresh, signal, waitMs) {
|
|
138
161
|
if (!signal && (!waitMs || waitMs <= 0))
|
|
@@ -293,7 +316,17 @@ export class FreshnessCoordinator {
|
|
|
293
316
|
this.#pending.delete(pendingRoot);
|
|
294
317
|
}
|
|
295
318
|
if (state.inFlight) {
|
|
296
|
-
|
|
319
|
+
const shared = state.inFlight;
|
|
320
|
+
try {
|
|
321
|
+
await attachRefreshWaiter(state, shared, options.signal, this.#waitBudget(options));
|
|
322
|
+
}
|
|
323
|
+
catch (cause) {
|
|
324
|
+
if (!isForeignRefreshCancel(cause, options.signal))
|
|
325
|
+
throw cause;
|
|
326
|
+
// Another caller's cancel tore down the shared refresh. This caller is
|
|
327
|
+
// still alive: settle the dead promise, then decide for itself below.
|
|
328
|
+
await shared.catch(() => undefined);
|
|
329
|
+
}
|
|
297
330
|
return this.ensureFresh(runtime, rootContext, options);
|
|
298
331
|
}
|
|
299
332
|
if (options.signal?.aborted)
|
|
@@ -2,7 +2,7 @@ import { type MachineEnvelope } from "./types.js";
|
|
|
2
2
|
export type IndexHealth = "ready" | "missing" | "incompatible";
|
|
3
3
|
export declare function pathContained(parent: string, child: string): boolean;
|
|
4
4
|
export declare function record(value: unknown): Record<string, unknown> | undefined;
|
|
5
|
-
export declare function indexHealth(status: MachineEnvelope
|
|
5
|
+
export declare function indexHealth(status: MachineEnvelope): IndexHealth;
|
|
6
6
|
export declare function incompatibleStatusFailure(cause: unknown): boolean;
|
|
7
7
|
export declare function indexCompletion(response: MachineEnvelope, requireWalkErrors: boolean): {
|
|
8
8
|
failed: number;
|
|
@@ -14,7 +14,7 @@ export function pathContained(parent, child) {
|
|
|
14
14
|
export function record(value) {
|
|
15
15
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
16
16
|
}
|
|
17
|
-
export function indexHealth(status
|
|
17
|
+
export function indexHealth(status) {
|
|
18
18
|
const index = record(status.index);
|
|
19
19
|
const state = typeof index?.status === "string" ? index.status :
|
|
20
20
|
typeof status.index_status === "string" ? status.index_status : undefined;
|
|
@@ -25,7 +25,9 @@ export function indexHealth(status, knownExisting = false) {
|
|
|
25
25
|
if (state === "ready" || state === "current" || index?.exists === true || status.indexed === true)
|
|
26
26
|
return "ready";
|
|
27
27
|
if (typeof status.index_path === "string" && typeof status.file_count === "number") {
|
|
28
|
-
|
|
28
|
+
// A present-but-empty index answers nothing: report it as unindexed so the
|
|
29
|
+
// caller indexes instead of reading zero rows as a legitimate no-match.
|
|
30
|
+
return status.file_count > 0 ? "ready" : "missing";
|
|
29
31
|
}
|
|
30
32
|
throw new RuntimeError("INDEX_STATUS_UNKNOWN", "ast-sgrep status did not report index freshness", { index: status.index, index_status: status.index_status });
|
|
31
33
|
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared leaf module: version constants, wire types, RuntimeError.
|
|
3
3
|
* Imports nothing from sibling modules — any file may depend on it.
|
|
4
4
|
*/
|
|
5
|
-
export declare const RUNTIME_VERSION = "2.
|
|
5
|
+
export declare const RUNTIME_VERSION = "2.1.0";
|
|
6
6
|
export declare const MACHINE_SCHEMA_VERSION = "1.0.0";
|
|
7
7
|
export declare const CONFIG_SCHEMA_VERSION: 1;
|
|
8
8
|
/** Index format this release ships. Must equal INDEX_SCHEMA_VERSION in crates/ast-sgrep-core (check-contract gates it). */
|
package/dist/runtime/types.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared leaf module: version constants, wire types, RuntimeError.
|
|
3
3
|
* Imports nothing from sibling modules — any file may depend on it.
|
|
4
4
|
*/
|
|
5
|
-
export const RUNTIME_VERSION = "2.
|
|
5
|
+
export const RUNTIME_VERSION = "2.1.0";
|
|
6
6
|
export const MACHINE_SCHEMA_VERSION = "1.0.0";
|
|
7
7
|
export const CONFIG_SCHEMA_VERSION = 1;
|
|
8
8
|
/** Index format this release ships. Must equal INDEX_SCHEMA_VERSION in crates/ast-sgrep-core (check-contract gates it). */
|
package/dist/ui/card.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export type CardModel = {
|
|
|
26
26
|
ms: number;
|
|
27
27
|
}>;
|
|
28
28
|
resultLines?: string[];
|
|
29
|
+
/** Warnings that qualify the answer (stale index, unindexed repo). */
|
|
30
|
+
notes?: string[];
|
|
29
31
|
error?: string;
|
|
30
32
|
running?: boolean;
|
|
31
33
|
expanded?: boolean;
|
|
@@ -55,9 +57,10 @@ type RenderOptions = {
|
|
|
55
57
|
};
|
|
56
58
|
type RenderContext = {
|
|
57
59
|
lastComponent?: unknown;
|
|
60
|
+
args?: object;
|
|
58
61
|
};
|
|
59
62
|
/** Build the card model from the tool result's details payload. */
|
|
60
|
-
export declare function cardModel(result: ResultLike, options: RenderOptions): CardModel;
|
|
63
|
+
export declare function cardModel(result: ResultLike, options: RenderOptions, callArgs?: Record<string, unknown>): CardModel;
|
|
61
64
|
/** renderResult entrypoint: bind one card per result slot, feed it details. */
|
|
62
65
|
export declare function renderAsgrepResult(result: ResultLike, options: RenderOptions, theme: PresentTheme, context?: RenderContext): AsgrepCard;
|
|
63
66
|
export {};
|
package/dist/ui/card.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Pi TUI result card — supernova-style: the call slot is empty; one card
|
|
2
2
|
* owns the whole lifecycle (running → ops ledger → result → error). Rows are
|
|
3
3
|
* fixed-column and theme-painted; nothing here writes to the model channel. */
|
|
4
|
-
import { hitLabel, hitLocation, paint, truncateToWidth, visibleWidth, } from "./present.js";
|
|
4
|
+
import { displayWidth, hitLabel, hitLocation, paint, sanitizeContent, summarizeValue, truncateToWidth, visibleWidth, } from "./present.js";
|
|
5
5
|
/** Header text that rides the top border: "asgrep search · 4 hits · 12ms · napi". */
|
|
6
6
|
function frameLabel(model) {
|
|
7
7
|
const bits = ["asgrep", model.command, ...model.title.filter((b) => Boolean(b))];
|
|
@@ -22,7 +22,6 @@ export const EMPTY_CALL = {
|
|
|
22
22
|
};
|
|
23
23
|
const TOOL_COL = 12;
|
|
24
24
|
const DUR_COL = 7;
|
|
25
|
-
const MAX_CARD_WIDTH = 100;
|
|
26
25
|
/** Rounded frame glyphs — pi themes may provide theme.boxRound; default ASCII-art set. */
|
|
27
26
|
const BOX = { tl: "\u256d", tr: "\u256e", bl: "\u2570", br: "\u256f", h: "\u2500", v: "\u2502" };
|
|
28
27
|
function boxOf(theme) {
|
|
@@ -39,45 +38,54 @@ function borderKey(model) {
|
|
|
39
38
|
return "accent";
|
|
40
39
|
return "dim";
|
|
41
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
|
+
}
|
|
42
71
|
/** Top/bottom bar with an optional label embedded in the rule. Geometry is in
|
|
43
|
-
* display cells (
|
|
72
|
+
* display cells (displayWidth): \u256d + 3 rules on the left, corner on the right. */
|
|
44
73
|
function frameBar(theme, box, border, left, right, label, width) {
|
|
45
74
|
const leftRaw = left + box.h.repeat(3);
|
|
46
|
-
const shown = label ? clamp(" " + label + " ", Math.max(0, width -
|
|
47
|
-
const fill = Math.max(0, width -
|
|
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);
|
|
48
77
|
return border(leftRaw) + shown + border(box.h.repeat(fill)) + border(right);
|
|
49
78
|
}
|
|
50
|
-
/**
|
|
51
|
-
* (
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
+
*/
|
|
54
87
|
function clamp(text, width) {
|
|
55
|
-
|
|
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";
|
|
88
|
+
return truncateToWidth(text, Math.max(1, width), "\u2026");
|
|
81
89
|
}
|
|
82
90
|
function fitPath(text, budget) {
|
|
83
91
|
if (visibleWidth(text) <= budget)
|
|
@@ -86,12 +94,6 @@ function fitPath(text, budget) {
|
|
|
86
94
|
return "\u2026";
|
|
87
95
|
return "\u2026" + text.slice(Math.max(0, text.length - budget + 1));
|
|
88
96
|
}
|
|
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
97
|
function fmtMs(ms) {
|
|
96
98
|
if (!Number.isFinite(ms) || ms < 0)
|
|
97
99
|
return "";
|
|
@@ -134,9 +136,11 @@ export class AsgrepCard {
|
|
|
134
136
|
return [];
|
|
135
137
|
if (this.cache?.width === width)
|
|
136
138
|
return this.cache.lines;
|
|
137
|
-
// Pi hands us the full terminal width
|
|
138
|
-
//
|
|
139
|
-
|
|
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));
|
|
140
144
|
this.cache = { width, lines };
|
|
141
145
|
return lines;
|
|
142
146
|
}
|
|
@@ -149,13 +153,15 @@ function framedLines(theme, model, width) {
|
|
|
149
153
|
const inner = Math.max(1, width - 4); // "\u2502 " + content + " \u2502"
|
|
150
154
|
const label = frameLabel(model);
|
|
151
155
|
const rows = bodyLines(theme, model, inner);
|
|
152
|
-
const
|
|
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))];
|
|
153
159
|
for (const row of rows) {
|
|
154
160
|
const body = clamp(row, inner);
|
|
155
|
-
const pad = Math.max(0, inner -
|
|
156
|
-
out.push(border(box.v) + " " + body + " ".repeat(pad) + " " + border(box.v));
|
|
161
|
+
const pad = Math.max(0, inner - displayWidth(body));
|
|
162
|
+
out.push(fill(border(box.v) + " " + body + " ".repeat(pad) + " " + border(box.v)));
|
|
157
163
|
}
|
|
158
|
-
out.push(frameBar(theme, box, border, box.bl, box.br, null, width));
|
|
164
|
+
out.push(fill(frameBar(theme, box, border, box.bl, box.br, null, width)));
|
|
159
165
|
return out;
|
|
160
166
|
}
|
|
161
167
|
function bodyLines(theme, model, width) {
|
|
@@ -174,13 +180,13 @@ function bodyLines(theme, model, width) {
|
|
|
174
180
|
lines.push(paint(theme, "dim", " \u2026 " + ((model.hits?.length ?? 0) - hits.length) + " more"));
|
|
175
181
|
}
|
|
176
182
|
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) : "");
|
|
183
|
+
const head = " " + paint(theme, "accent", sanitizeContent(edit.path ?? "?")) + (edit.line ? paint(theme, "dim", ":" + edit.line) : "");
|
|
178
184
|
lines.push(clamp(head, width));
|
|
179
185
|
for (const line of (edit.removed ?? []).slice(0, model.expanded ? 24 : 8)) {
|
|
180
|
-
lines.push(" " + paint(theme, "error", "- " + clamp(line, width -
|
|
186
|
+
lines.push(" " + paint(theme, "error", "- " + clamp(line, width - 5)));
|
|
181
187
|
}
|
|
182
188
|
for (const line of (edit.added ?? []).slice(0, model.expanded ? 24 : 8)) {
|
|
183
|
-
lines.push(" " + paint(theme, "success", "+ " + clamp(line, width -
|
|
189
|
+
lines.push(" " + paint(theme, "success", "+ " + clamp(line, width - 5)));
|
|
184
190
|
}
|
|
185
191
|
if (edit.truncated)
|
|
186
192
|
lines.push(paint(theme, "dim", " \u2026"));
|
|
@@ -198,11 +204,49 @@ function bodyLines(theme, model, width) {
|
|
|
198
204
|
lines.push(paint(theme, "dim", " \u2026 " + (model.resultLines.length - shown.length) + " more result lines"));
|
|
199
205
|
}
|
|
200
206
|
}
|
|
207
|
+
for (const note of model.notes ?? []) {
|
|
208
|
+
lines.push(" " + paint(theme, "warning", "! " + clamp(note, width - 3)));
|
|
209
|
+
}
|
|
201
210
|
return lines;
|
|
202
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
|
+
}
|
|
203
231
|
function editsOf(value) {
|
|
204
232
|
if (value && typeof value === "object" && Array.isArray(value.edits)) {
|
|
205
|
-
return 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
|
+
});
|
|
206
250
|
}
|
|
207
251
|
return undefined;
|
|
208
252
|
}
|
|
@@ -216,37 +260,44 @@ function resultPreviewLines(value) {
|
|
|
216
260
|
if (value === undefined || value === null)
|
|
217
261
|
return undefined;
|
|
218
262
|
if (typeof value === "string")
|
|
219
|
-
return value.split("\n").filter((
|
|
263
|
+
return value.split("\n").filter((line) => line.length > 0).slice(0, 16).map(sanitizeContent);
|
|
220
264
|
if (Array.isArray(value))
|
|
221
265
|
return [value.length + " value" + (value.length === 1 ? "" : "s")];
|
|
222
266
|
if (typeof value === "object") {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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;
|
|
227
272
|
}
|
|
228
|
-
return [String(value)];
|
|
273
|
+
return [sanitizeContent(String(value))];
|
|
229
274
|
}
|
|
230
275
|
/** Build the card model from the tool result's details payload. */
|
|
231
|
-
export function cardModel(result, options) {
|
|
276
|
+
export function cardModel(result, options, callArgs) {
|
|
232
277
|
const details = (result.details && typeof result.details === "object" ? result.details : {});
|
|
233
278
|
const command = typeof details.command === "string" ? details.command : "asgrep";
|
|
234
279
|
const expanded = options.expanded === true;
|
|
235
280
|
// In-flight partial updates carry only {command, phase}.
|
|
236
281
|
if (options.isPartial && !("ok" in details)) {
|
|
237
|
-
|
|
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
|
+
};
|
|
238
289
|
}
|
|
239
290
|
const title = [];
|
|
240
291
|
if (typeof details.query === "string" && details.query)
|
|
241
|
-
title.push(JSON.stringify(details.query));
|
|
292
|
+
title.push(JSON.stringify(sanitizeContent(details.query)));
|
|
242
293
|
if (typeof details.mode === "string")
|
|
243
|
-
title.push(details.mode);
|
|
294
|
+
title.push(sanitizeContent(details.mode));
|
|
244
295
|
const error = details.error;
|
|
245
296
|
if (result.isError || details.ok === false || error) {
|
|
246
297
|
return {
|
|
247
298
|
command,
|
|
248
299
|
title,
|
|
249
|
-
error: typeof error?.message === "string" ? error.message : "tool failed",
|
|
300
|
+
error: typeof error?.message === "string" ? sanitizeContent(error.message) : "tool failed",
|
|
250
301
|
expanded,
|
|
251
302
|
};
|
|
252
303
|
}
|
|
@@ -289,8 +340,8 @@ export function cardModel(result, options) {
|
|
|
289
340
|
? response.windows
|
|
290
341
|
: undefined;
|
|
291
342
|
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) : []),
|
|
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) : []),
|
|
294
345
|
]);
|
|
295
346
|
// When edits carry diffs they are the interesting part of the result.
|
|
296
347
|
const resultLines = hits || resultEdits ? undefined : (readLines ?? resultPreviewLines(details.result));
|
|
@@ -303,13 +354,21 @@ export function cardModel(result, options) {
|
|
|
303
354
|
model.edits = resultEdits;
|
|
304
355
|
if (resultLines)
|
|
305
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
|
+
}
|
|
306
364
|
return model;
|
|
307
365
|
}
|
|
308
366
|
/** renderResult entrypoint: bind one card per result slot, feed it details. */
|
|
309
367
|
export function renderAsgrepResult(result, options, theme, context) {
|
|
310
368
|
const prev = context?.lastComponent;
|
|
311
369
|
const card = prev instanceof AsgrepCard ? prev : new AsgrepCard();
|
|
312
|
-
|
|
370
|
+
const args = context?.args;
|
|
371
|
+
card.set(theme, cardModel(result, options, args && !Array.isArray(args) ? args : undefined));
|
|
313
372
|
if (context)
|
|
314
373
|
context.lastComponent = card;
|
|
315
374
|
return card;
|
package/dist/ui/present.d.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export type PresentTheme = {
|
|
3
3
|
bold(text: string): string;
|
|
4
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;
|
|
5
7
|
};
|
|
6
8
|
export type HitLike = {
|
|
7
9
|
file?: unknown;
|
|
@@ -25,47 +27,55 @@ export type EnvelopeLike = {
|
|
|
25
27
|
backend?: unknown;
|
|
26
28
|
[key: string]: unknown;
|
|
27
29
|
};
|
|
28
|
-
export declare const ASGREP_PROMPT_SNIPPET = "
|
|
29
|
-
export declare const ASGREP_PROMPT_GUIDELINES: readonly ["
|
|
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."];
|
|
30
32
|
export declare function paint(theme: PresentTheme | undefined, role: string, text: string, bold?: boolean): string;
|
|
31
33
|
export declare function hitLocation(hit: HitLike): string;
|
|
32
34
|
export declare function hitLabel(hit: HitLike): string;
|
|
33
|
-
export declare function header(theme: PresentTheme | undefined, verb: string, bits: Array<string | null | undefined>): string;
|
|
34
|
-
export declare function formatSearchCall(params: {
|
|
35
|
-
query?: string;
|
|
36
|
-
mode?: string;
|
|
37
|
-
limit?: number;
|
|
38
|
-
excerptLines?: number;
|
|
39
|
-
}, theme?: PresentTheme): string;
|
|
40
|
-
export declare function formatIndexCall(force: boolean, theme?: PresentTheme): string;
|
|
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
35
|
export declare function formatEditResult(response: EnvelopeLike, theme?: PresentTheme): string;
|
|
54
36
|
/** Model-visible text for a read envelope: the window contents themselves. */
|
|
55
37
|
export declare function formatReadResult(response: EnvelopeLike, theme?: PresentTheme): string;
|
|
56
|
-
|
|
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
|
+
*/
|
|
57
44
|
export declare function formatSearchResult(response: EnvelopeLike, meta: {
|
|
58
45
|
command: string;
|
|
59
|
-
|
|
60
|
-
mode?: string;
|
|
61
|
-
activationMs?: number;
|
|
62
|
-
backend?: string;
|
|
46
|
+
excerptLines?: number;
|
|
63
47
|
}, theme?: PresentTheme): string;
|
|
64
48
|
export declare function formatStatusResult(response: EnvelopeLike, theme?: PresentTheme): string;
|
|
65
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;
|
|
66
70
|
/** Local stand-in so we do not take a pi-tui dependency. Over-counts wide glyphs rather than under-count. */
|
|
67
71
|
export declare function visibleWidth(text: string): number;
|
|
68
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[];
|
|
69
79
|
export declare function formatCodemodeResult(value: unknown, meta?: {
|
|
70
80
|
stats?: {
|
|
71
81
|
calls: number;
|
|
@@ -77,12 +87,3 @@ export declare function formatCodemodeResult(value: unknown, meta?: {
|
|
|
77
87
|
wallMs?: number;
|
|
78
88
|
backend?: string;
|
|
79
89
|
}, theme?: PresentTheme): string;
|
|
80
|
-
/** Minimal pi-tui Text stand-in so we do not take a TUI package dependency. */
|
|
81
|
-
export declare class AsgrepText {
|
|
82
|
-
#private;
|
|
83
|
-
constructor(text?: string);
|
|
84
|
-
setText(text: string): void;
|
|
85
|
-
invalidate(): void;
|
|
86
|
-
render(width: number): string[];
|
|
87
|
-
}
|
|
88
|
-
export declare function presentText(formatted: string, last: unknown): AsgrepText;
|