@heyhuynhgiabuu/pi-pretty 0.4.1 → 0.4.3
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 -1
- package/package.json +1 -1
- package/release-notes/v0.4.2.md +36 -0
- package/src/fff-helpers.ts +16 -4
- package/src/index.ts +225 -130
- package/src/multi-grep-fallback.ts +248 -0
- package/test/bash-rendering.test.ts +109 -0
- package/test/fff-integration.test.ts +284 -9
- package/test/image-rendering.test.ts +7 -25
package/README.md
CHANGED
|
@@ -135,7 +135,7 @@ Use them when:
|
|
|
135
135
|
|
|
136
136
|
Optional environment variables:
|
|
137
137
|
|
|
138
|
-
- `PRETTY_THEME` (
|
|
138
|
+
- `PRETTY_THEME` (overrides `~/.pi/agent/settings.json` `theme`; otherwise pi-pretty falls back to that setting before `github-dark`)
|
|
139
139
|
- `PRETTY_MAX_HL_CHARS` (default: `80000`)
|
|
140
140
|
- `PRETTY_MAX_PREVIEW_LINES` (default: `80`)
|
|
141
141
|
- `PRETTY_CACHE_LIMIT` (default: `128`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyhuynhgiabuu/pi-pretty",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Pretty terminal output for pi — syntax-highlighted file reads, colored bash output, tree-view directory listings, and more.",
|
|
5
5
|
"author": "huynhgiabuu",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# pi-pretty 0.4.2
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
This patch release merges the reviewed `customizations` PR into `main` and ships safer `multi_grep` fallback behavior, improved theme resolution, normalized output handling, and clearer bash command rendering.
|
|
5
|
+
|
|
6
|
+
## What changed
|
|
7
|
+
- `multi_grep` now preserves constrained searches by routing them through a dedicated ripgrep fallback instead of silently dropping complex constraints.
|
|
8
|
+
- Complex constraint expressions such as `*.ts !test/` are converted into ripgrep `--glob` filters, while invalid constraint syntax now returns an error instead of being ignored.
|
|
9
|
+
- Constrained `grep` safety from `0.4.1` remains intact: `path`/`glob` constrained searches still avoid native FFF.
|
|
10
|
+
- pi-pretty now reads `~/.pi/agent/settings.json` theme settings by default, while `PRETTY_THEME` continues to override explicitly.
|
|
11
|
+
- Expanded bash tool rendering so expanded tool calls show the full command instead of the truncated preview.
|
|
12
|
+
- Normalized read/grep output handling and tightened single-line-safe FFF grep record formatting.
|
|
13
|
+
- Added test coverage for bash rendering, constrained grep and multi_grep fallback behavior, CRLF handling, and ripgrep constraint preservation.
|
|
14
|
+
|
|
15
|
+
## Files
|
|
16
|
+
- `src/index.ts`
|
|
17
|
+
- `src/fff-helpers.ts`
|
|
18
|
+
- `src/multi-grep-fallback.ts`
|
|
19
|
+
- `test/bash-rendering.test.ts`
|
|
20
|
+
- `test/fff-integration.test.ts`
|
|
21
|
+
- `README.md`
|
|
22
|
+
- `package.json`
|
|
23
|
+
- `package-lock.json`
|
|
24
|
+
|
|
25
|
+
## Verification
|
|
26
|
+
- `npm run typecheck` ✅
|
|
27
|
+
- `npm run lint` ✅
|
|
28
|
+
- `npm test` ✅ (67 tests)
|
|
29
|
+
|
|
30
|
+
## Upgrade notes
|
|
31
|
+
No configuration changes are required.
|
|
32
|
+
|
|
33
|
+
After updating:
|
|
34
|
+
- constrained `grep` and `multi_grep` searches prefer safe fallback paths instead of unsafe native constrained matching
|
|
35
|
+
- complex `multi_grep` constraints are preserved instead of being widened silently
|
|
36
|
+
- theme selection can follow Pi settings automatically unless `PRETTY_THEME` is set
|
package/src/fff-helpers.ts
CHANGED
|
@@ -6,6 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
import type { GrepCursor, GrepMatch } from "@ff-labs/fff-node";
|
|
8
8
|
|
|
9
|
+
function sanitizeGrepRecordContent(text: string): string {
|
|
10
|
+
let content = text;
|
|
11
|
+
if (content.endsWith("\r\n")) content = content.slice(0, -2);
|
|
12
|
+
else if (content.endsWith("\r") || content.endsWith("\n")) content = content.slice(0, -1);
|
|
13
|
+
|
|
14
|
+
return content.replace(/\r\n/g, "\\n").replace(/\r/g, "\\r").replace(/\n/g, "\\n");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function truncateGrepRecordContent(text: string): string {
|
|
18
|
+
const content = sanitizeGrepRecordContent(text);
|
|
19
|
+
return content.length > 500 ? `${content.slice(0, 500)}...` : content;
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
/**
|
|
10
23
|
* Store for FFF grep pagination cursors.
|
|
11
24
|
* Evicts oldest entry when exceeding maxSize.
|
|
@@ -57,15 +70,14 @@ export function fffFormatGrepText(items: GrepMatch[], limit: number): string {
|
|
|
57
70
|
if (match.contextBefore?.length) {
|
|
58
71
|
const startLine = match.lineNumber - match.contextBefore.length;
|
|
59
72
|
for (let i = 0; i < match.contextBefore.length; i++) {
|
|
60
|
-
lines.push(`${match.relativePath}-${startLine + i}-${match.contextBefore[i]}`);
|
|
73
|
+
lines.push(`${match.relativePath}-${startLine + i}-${truncateGrepRecordContent(match.contextBefore[i] ?? "")}`);
|
|
61
74
|
}
|
|
62
75
|
}
|
|
63
|
-
|
|
64
|
-
lines.push(`${match.relativePath}:${match.lineNumber}:${content}`);
|
|
76
|
+
lines.push(`${match.relativePath}:${match.lineNumber}:${truncateGrepRecordContent(match.lineContent)}`);
|
|
65
77
|
if (match.contextAfter?.length) {
|
|
66
78
|
const startLine = match.lineNumber + 1;
|
|
67
79
|
for (let i = 0; i < match.contextAfter.length; i++) {
|
|
68
|
-
lines.push(`${match.relativePath}-${startLine + i}-${match.contextAfter[i]}`);
|
|
80
|
+
lines.push(`${match.relativePath}-${startLine + i}-${truncateGrepRecordContent(match.contextAfter[i] ?? "")}`);
|
|
69
81
|
}
|
|
70
82
|
}
|
|
71
83
|
}
|
package/src/index.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import * as childProcess from "node:child_process";
|
|
27
|
-
import { mkdirSync } from "node:fs";
|
|
27
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
28
28
|
import { basename, dirname, extname, join, relative } from "node:path";
|
|
29
29
|
|
|
30
30
|
import type { FileFinder, FileItem, GrepResult, SearchResult } from "@ff-labs/fff-node";
|
|
@@ -45,12 +45,46 @@ import { codeToANSI } from "@shikijs/cli";
|
|
|
45
45
|
import type { BundledLanguage, BundledTheme } from "shiki";
|
|
46
46
|
|
|
47
47
|
import { CursorStore, fffFormatGrepText } from "./fff-helpers.js";
|
|
48
|
+
import { type MultiGrepRipgrepFallback, runMultiGrepRipgrepFallback } from "./multi-grep-fallback.js";
|
|
48
49
|
|
|
49
50
|
// ---------------------------------------------------------------------------
|
|
50
51
|
// Config
|
|
51
52
|
// ---------------------------------------------------------------------------
|
|
52
53
|
|
|
53
|
-
const
|
|
54
|
+
const DEFAULT_THEME: BundledTheme = "github-dark";
|
|
55
|
+
|
|
56
|
+
function getDefaultAgentDir(): string | undefined {
|
|
57
|
+
const home = process.env.HOME ?? "";
|
|
58
|
+
return home ? join(home, ".pi/agent") : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readThemeFromSettings(agentDir?: string): BundledTheme | undefined {
|
|
62
|
+
const resolvedAgentDir = agentDir ?? getDefaultAgentDir();
|
|
63
|
+
if (!resolvedAgentDir) return undefined;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const settings = JSON.parse(readFileSync(join(resolvedAgentDir, "settings.json"), "utf8")) as {
|
|
67
|
+
theme?: unknown;
|
|
68
|
+
};
|
|
69
|
+
return typeof settings.theme === "string" ? (settings.theme as BundledTheme) : undefined;
|
|
70
|
+
} catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolvePrettyTheme(agentDir?: string): BundledTheme {
|
|
76
|
+
return (process.env.PRETTY_THEME as BundledTheme | undefined) ?? readThemeFromSettings(agentDir) ?? DEFAULT_THEME;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let THEME: BundledTheme = resolvePrettyTheme();
|
|
80
|
+
|
|
81
|
+
function setPrettyTheme(agentDir?: string): void {
|
|
82
|
+
const resolvedTheme = resolvePrettyTheme(agentDir);
|
|
83
|
+
if (resolvedTheme === THEME) return;
|
|
84
|
+
THEME = resolvedTheme;
|
|
85
|
+
_cache.clear();
|
|
86
|
+
codeToANSI("", "typescript", THEME).catch(() => {});
|
|
87
|
+
}
|
|
54
88
|
|
|
55
89
|
function envInt(name: string, fallback: number): number {
|
|
56
90
|
const v = Number.parseInt(process.env[name] ?? "", 10);
|
|
@@ -100,13 +134,11 @@ function getThemeBgAnsi(theme: BgTheme, key: string): string | null {
|
|
|
100
134
|
}
|
|
101
135
|
|
|
102
136
|
/** Read themed tool backgrounds and update BG_BASE / BG_ERROR + RST.
|
|
103
|
-
*
|
|
104
|
-
let _bgBaseResolved = false;
|
|
137
|
+
* Recompute on each render so runtime theme changes are respected. */
|
|
105
138
|
function resolveBaseBackground(theme: BgTheme | null | undefined): void {
|
|
106
|
-
if (
|
|
107
|
-
_bgBaseResolved = true;
|
|
139
|
+
if (!theme?.getBgAnsi) return;
|
|
108
140
|
|
|
109
|
-
BG_BASE = getThemeBgAnsi(theme, "
|
|
141
|
+
BG_BASE = getThemeBgAnsi(theme, "toolBg") ?? getThemeBgAnsi(theme, "background") ?? BG_DEFAULT;
|
|
110
142
|
BG_ERROR = getThemeBgAnsi(theme, "toolErrorBg") ?? BG_BASE;
|
|
111
143
|
RST = `\x1b[0m${BG_BASE}`;
|
|
112
144
|
}
|
|
@@ -146,6 +178,10 @@ function strip(s: string): string {
|
|
|
146
178
|
return s.replace(ANSI_RE, "");
|
|
147
179
|
}
|
|
148
180
|
|
|
181
|
+
function normalizeLineEndings(text: string): string {
|
|
182
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
183
|
+
}
|
|
184
|
+
|
|
149
185
|
function preserveToolBackground(ansi: string, bg: string): string {
|
|
150
186
|
return ansi.replace(ANSI_CAPTURE_RE, (seq, params: string) => {
|
|
151
187
|
const codes = params.split(";");
|
|
@@ -402,47 +438,6 @@ export const __imageInternals = {
|
|
|
402
438
|
},
|
|
403
439
|
};
|
|
404
440
|
|
|
405
|
-
/**
|
|
406
|
-
* Render base64 image inline using iTerm2 inline image protocol.
|
|
407
|
-
* Protocol: ESC ] 1337 ; File=[args] : base64data BEL
|
|
408
|
-
*/
|
|
409
|
-
function renderIterm2Image(base64Data: string, opts: { width?: string; name?: string } = {}): string {
|
|
410
|
-
const args: string[] = ["inline=1", "preserveAspectRatio=1"];
|
|
411
|
-
if (opts.width) args.push(`width=${opts.width}`);
|
|
412
|
-
if (opts.name) args.push(`name=${Buffer.from(opts.name).toString("base64")}`);
|
|
413
|
-
const byteSize = Math.ceil((base64Data.length * 3) / 4);
|
|
414
|
-
args.push(`size=${byteSize}`);
|
|
415
|
-
const seq = `\x1b]1337;File=${args.join(";")}:${base64Data}\x07`;
|
|
416
|
-
return tmuxWrap(seq);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Render base64 image inline using Kitty graphics protocol.
|
|
421
|
-
* Protocol: ESC _G <key>=<value>,...; <base64data> ESC \
|
|
422
|
-
* Chunked in 4096-byte pieces as required by protocol.
|
|
423
|
-
* Supported by: Kitty, Ghostty
|
|
424
|
-
*/
|
|
425
|
-
function renderKittyImage(base64Data: string, opts: { cols?: number } = {}): string {
|
|
426
|
-
const chunks: string[] = [];
|
|
427
|
-
const CHUNK_SIZE = 4096;
|
|
428
|
-
|
|
429
|
-
for (let i = 0; i < base64Data.length; i += CHUNK_SIZE) {
|
|
430
|
-
const chunk = base64Data.slice(i, i + CHUNK_SIZE);
|
|
431
|
-
const isFirst = i === 0;
|
|
432
|
-
const isLast = i + CHUNK_SIZE >= base64Data.length;
|
|
433
|
-
const more = isLast ? 0 : 1;
|
|
434
|
-
|
|
435
|
-
if (isFirst) {
|
|
436
|
-
const colPart = opts.cols ? `,c=${opts.cols}` : "";
|
|
437
|
-
chunks.push(tmuxWrap(`\x1b_Ga=T,f=100,t=d,m=${more}${colPart};${chunk}\x1b\\`));
|
|
438
|
-
} else {
|
|
439
|
-
chunks.push(tmuxWrap(`\x1b_Gm=${more};${chunk}\x1b\\`));
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
return chunks.join("");
|
|
444
|
-
}
|
|
445
|
-
|
|
446
441
|
/**
|
|
447
442
|
* Get human-readable file size
|
|
448
443
|
*/
|
|
@@ -614,7 +609,8 @@ async function renderFileContent(
|
|
|
614
609
|
offset = 1,
|
|
615
610
|
maxLines = MAX_PREVIEW_LINES,
|
|
616
611
|
): Promise<string> {
|
|
617
|
-
const
|
|
612
|
+
const normalizedContent = normalizeLineEndings(content);
|
|
613
|
+
const lines = normalizedContent.split("\n");
|
|
618
614
|
const total = lines.length;
|
|
619
615
|
const show = lines.slice(0, maxLines);
|
|
620
616
|
const lg = lang(filePath);
|
|
@@ -754,7 +750,7 @@ function renderFindResults(text: string): string {
|
|
|
754
750
|
|
|
755
751
|
/** Render grep results with highlighted matches and line numbers. */
|
|
756
752
|
async function renderGrepResults(text: string, pattern: string): Promise<string> {
|
|
757
|
-
const lines = text.split("\n");
|
|
753
|
+
const lines = normalizeLineEndings(text).split("\n");
|
|
758
754
|
if (!lines.length || (lines.length === 1 && !lines[0].trim())) return `${FG_DIM}(no matches)${RST}`;
|
|
759
755
|
|
|
760
756
|
const out: string[] = [];
|
|
@@ -875,6 +871,7 @@ type FindParams = FindToolInput;
|
|
|
875
871
|
type GrepParams = GrepToolInput;
|
|
876
872
|
type MultiGrepParams = {
|
|
877
873
|
patterns: string[];
|
|
874
|
+
path?: string;
|
|
878
875
|
constraints?: string;
|
|
879
876
|
context?: number;
|
|
880
877
|
limit?: number;
|
|
@@ -934,6 +931,34 @@ function getErrorMessage(error: unknown): string {
|
|
|
934
931
|
return error instanceof Error ? error.message : String(error);
|
|
935
932
|
}
|
|
936
933
|
|
|
934
|
+
function trimToUndefined(value: string | undefined): string | undefined {
|
|
935
|
+
const trimmed = value?.trim();
|
|
936
|
+
return trimmed ? trimmed : undefined;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function escapeRegexLiteral(text: string): string {
|
|
940
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function buildLiteralAlternationPattern(patterns: string[]): string {
|
|
944
|
+
return patterns
|
|
945
|
+
.map(escapeRegexLiteral)
|
|
946
|
+
.sort((a, b) => b.length - a.length)
|
|
947
|
+
.join("|");
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function shouldIgnoreCaseForPatterns(patterns: string[]): boolean {
|
|
951
|
+
return patterns.every((pattern) => pattern.toLowerCase() === pattern);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function getConstraintBackedPath(constraints: string | undefined): string | undefined {
|
|
955
|
+
const trimmed = trimToUndefined(constraints);
|
|
956
|
+
if (!trimmed || /\s/.test(trimmed) || trimmed.includes("!") || trimmed.endsWith("/") || /[*?[{]/.test(trimmed)) {
|
|
957
|
+
return undefined;
|
|
958
|
+
}
|
|
959
|
+
return trimmed;
|
|
960
|
+
}
|
|
961
|
+
|
|
937
962
|
const _cursorStore = new CursorStore();
|
|
938
963
|
let _fffModule: OptionalFffModule | null = null;
|
|
939
964
|
let _fffFinder: FffBackedFinder | null = null;
|
|
@@ -985,6 +1010,7 @@ export interface PiPrettyDeps {
|
|
|
985
1010
|
sdk: PiPrettySdk;
|
|
986
1011
|
TextComponent: TextComponentCtor;
|
|
987
1012
|
fffModule?: OptionalFffModule;
|
|
1013
|
+
multiGrepRipgrepFallback?: MultiGrepRipgrepFallback;
|
|
988
1014
|
}
|
|
989
1015
|
|
|
990
1016
|
export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps): void {
|
|
@@ -1028,12 +1054,22 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1028
1054
|
const cwd = process.cwd();
|
|
1029
1055
|
const home = process.env.HOME ?? "";
|
|
1030
1056
|
const sp = (p: string) => shortPath(cwd, home, p);
|
|
1057
|
+
const multiGrepRipgrepFallback = deps?.multiGrepRipgrepFallback ?? runMultiGrepRipgrepFallback;
|
|
1031
1058
|
|
|
1032
1059
|
// ===================================================================
|
|
1033
1060
|
// FFF initialization (optional — graceful fallback to SDK)
|
|
1034
1061
|
// ===================================================================
|
|
1035
1062
|
|
|
1036
1063
|
const getAgentDir = sdk.getAgentDir;
|
|
1064
|
+
setPrettyTheme(
|
|
1065
|
+
(() => {
|
|
1066
|
+
try {
|
|
1067
|
+
return getAgentDir?.() ?? getDefaultAgentDir();
|
|
1068
|
+
} catch {
|
|
1069
|
+
return getDefaultAgentDir();
|
|
1070
|
+
}
|
|
1071
|
+
})(),
|
|
1072
|
+
);
|
|
1037
1073
|
if (!deps) {
|
|
1038
1074
|
// Only try require() in production — tests inject fffModule via deps
|
|
1039
1075
|
try {
|
|
@@ -1077,8 +1113,9 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1077
1113
|
if (_fffPartialIndex) {
|
|
1078
1114
|
ctx.ui?.notify?.("FFF: scan timed out — using partial index. Run /fff-rescan when ready.", "warning");
|
|
1079
1115
|
} else {
|
|
1080
|
-
ctx.ui
|
|
1081
|
-
|
|
1116
|
+
const ui = ctx.ui;
|
|
1117
|
+
ui?.setStatus?.("fff", "FFF indexed");
|
|
1118
|
+
setTimeout(() => ui?.setStatus?.("fff", undefined), 3000);
|
|
1082
1119
|
}
|
|
1083
1120
|
} catch (error: unknown) {
|
|
1084
1121
|
ctx.ui?.notify?.(`FFF init failed: ${getErrorMessage(error)}`, "error");
|
|
@@ -1124,11 +1161,12 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1124
1161
|
|
|
1125
1162
|
const textContent = getTextContent(result);
|
|
1126
1163
|
if (textContent && fp) {
|
|
1127
|
-
const
|
|
1164
|
+
const normalizedContent = normalizeLineEndings(textContent);
|
|
1165
|
+
const lineCount = normalizedContent.split("\n").length;
|
|
1128
1166
|
setResultDetails(result, {
|
|
1129
1167
|
_type: "readFile",
|
|
1130
1168
|
filePath: fp,
|
|
1131
|
-
content:
|
|
1169
|
+
content: normalizedContent,
|
|
1132
1170
|
offset,
|
|
1133
1171
|
lineCount,
|
|
1134
1172
|
});
|
|
@@ -1162,45 +1200,15 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1162
1200
|
|
|
1163
1201
|
const d = result.details as RenderDetails | undefined;
|
|
1164
1202
|
|
|
1165
|
-
// Image
|
|
1203
|
+
// Image reads keep the original image content so Pi's native TUI renderer
|
|
1204
|
+
// can display it exactly once. pi-pretty only renders metadata here;
|
|
1205
|
+
// rendering another inline image caused duplicate previews.
|
|
1166
1206
|
if (d?._type === "readImage") {
|
|
1167
|
-
const tw = termW();
|
|
1168
|
-
const out: string[] = [];
|
|
1169
|
-
const fname = basename(d.filePath);
|
|
1170
1207
|
const byteSize = Math.ceil(((d.data as string).length * 3) / 4);
|
|
1171
1208
|
const sizeStr = humanSize(byteSize);
|
|
1172
1209
|
const mimeStr = d.mimeType ?? "image";
|
|
1173
1210
|
|
|
1174
|
-
|
|
1175
|
-
out.push(rule(tw));
|
|
1176
|
-
|
|
1177
|
-
const protocol = detectImageProtocol();
|
|
1178
|
-
const passthroughWarning = getTmuxPassthroughWarning(protocol);
|
|
1179
|
-
if (passthroughWarning) {
|
|
1180
|
-
out.push(` ${FG_YELLOW}${passthroughWarning}${RST}`);
|
|
1181
|
-
} else if (protocol === "kitty") {
|
|
1182
|
-
if (d.mimeType && d.mimeType !== "image/png") {
|
|
1183
|
-
out.push(
|
|
1184
|
-
` ${FG_YELLOW}Kitty/Ghostty inline preview currently supports PNG payloads (got ${d.mimeType})${RST}`,
|
|
1185
|
-
);
|
|
1186
|
-
} else {
|
|
1187
|
-
const imgCols = Math.min(tw - 4, 80);
|
|
1188
|
-
out.push(renderKittyImage(d.data, { cols: imgCols }));
|
|
1189
|
-
}
|
|
1190
|
-
} else if (protocol === "iterm2") {
|
|
1191
|
-
const imgWidth = Math.min(tw - 4, 80);
|
|
1192
|
-
out.push(
|
|
1193
|
-
renderIterm2Image(d.data, {
|
|
1194
|
-
width: `${imgWidth}`,
|
|
1195
|
-
name: fname,
|
|
1196
|
-
}),
|
|
1197
|
-
);
|
|
1198
|
-
} else {
|
|
1199
|
-
out.push(` ${FG_DIM}(Inline image preview requires Ghostty, iTerm2, WezTerm, or Kitty)${RST}`);
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
out.push(rule(tw));
|
|
1203
|
-
text.setText(fillToolBackground(out.join("\n")));
|
|
1211
|
+
text.setText(fillToolBackground(` ${fileIcon(d.filePath)}${FG_DIM}${mimeStr} · ${sizeStr}${RST}`));
|
|
1204
1212
|
return text;
|
|
1205
1213
|
}
|
|
1206
1214
|
|
|
@@ -1277,9 +1285,10 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1277
1285
|
const cmd = args.command ?? "";
|
|
1278
1286
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1279
1287
|
const timeout = args.timeout ? ` ${theme.fg("muted", `(${args.timeout}s timeout)`)}` : "";
|
|
1288
|
+
const displayCmd = ctx.expanded || cmd.length <= 80 ? cmd : `${cmd.slice(0, 77)}…`;
|
|
1280
1289
|
text.setText(
|
|
1281
1290
|
fillToolBackground(
|
|
1282
|
-
`${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("accent",
|
|
1291
|
+
`${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("accent", displayCmd)}${timeout}`,
|
|
1283
1292
|
),
|
|
1284
1293
|
);
|
|
1285
1294
|
return text;
|
|
@@ -1559,7 +1568,12 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1559
1568
|
|
|
1560
1569
|
// SDK fallback
|
|
1561
1570
|
const result = await origGrep.execute(tid, params, sig, upd as never, ctx);
|
|
1562
|
-
const textContent = getTextContent(result);
|
|
1571
|
+
const textContent = normalizeLineEndings(getTextContent(result));
|
|
1572
|
+
if (result.content) {
|
|
1573
|
+
for (const content of result.content) {
|
|
1574
|
+
if (isTextContent(content)) content.text = normalizeLineEndings(content.text || "");
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1563
1577
|
const matchCount = textContent ? countRipgrepMatches(textContent) : 0;
|
|
1564
1578
|
|
|
1565
1579
|
setResultDetails<GrepResultDetails>(result, {
|
|
@@ -1629,25 +1643,30 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1629
1643
|
}
|
|
1630
1644
|
|
|
1631
1645
|
// ===================================================================
|
|
1632
|
-
// multi_grep —
|
|
1646
|
+
// multi_grep — OR-logic multi-pattern search (FFF when available,
|
|
1647
|
+
// SDK grep fallback otherwise)
|
|
1633
1648
|
// ===================================================================
|
|
1634
1649
|
|
|
1635
|
-
if (_fffModule) {
|
|
1650
|
+
if (_fffModule || createGrepTool) {
|
|
1651
|
+
const multiGrepFallback = createGrepTool ? createGrepTool(cwd) : null;
|
|
1652
|
+
|
|
1636
1653
|
pi.registerTool({
|
|
1637
1654
|
name: "multi_grep",
|
|
1638
|
-
label: "multi_grep
|
|
1655
|
+
label: "multi_grep",
|
|
1639
1656
|
description: [
|
|
1640
1657
|
"Search file contents for lines matching ANY of multiple patterns (OR logic).",
|
|
1641
|
-
"Uses SIMD-accelerated Aho-Corasick multi-pattern matching
|
|
1658
|
+
"Uses SIMD-accelerated Aho-Corasick multi-pattern matching when FFF is available.",
|
|
1659
|
+
"Falls back to ripgrep while preserving literal OR semantics and file constraints when needed.",
|
|
1642
1660
|
"Patterns are literal text — never escape special characters.",
|
|
1643
|
-
"Use
|
|
1661
|
+
"Use path to scope a directory/file and constraints for file filtering ('*.rs', 'src/', '!test/').",
|
|
1644
1662
|
].join(" "),
|
|
1645
|
-
promptSnippet: "Multi-pattern OR search across file contents (FFF
|
|
1663
|
+
promptSnippet: "Multi-pattern OR search across file contents (FFF-accelerated with grep fallback)",
|
|
1646
1664
|
promptGuidelines: [
|
|
1647
1665
|
"Use multi_grep when you need to find multiple identifiers at once (OR logic).",
|
|
1648
1666
|
"Include all naming conventions: snake_case, PascalCase, camelCase variants.",
|
|
1649
1667
|
"Patterns are literal text. Never escape special characters.",
|
|
1650
|
-
"Use
|
|
1668
|
+
"Use path to scope a directory or file when you need fresh on-disk results.",
|
|
1669
|
+
"Use the constraints parameter for additional file filtering, not inside patterns.",
|
|
1651
1670
|
],
|
|
1652
1671
|
|
|
1653
1672
|
parameters: {
|
|
@@ -1658,6 +1677,10 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1658
1677
|
items: { type: "string" },
|
|
1659
1678
|
description: "Patterns to search for (OR logic — matches lines containing ANY pattern).",
|
|
1660
1679
|
},
|
|
1680
|
+
path: {
|
|
1681
|
+
type: "string",
|
|
1682
|
+
description: "Directory or file path to search (default: current directory)",
|
|
1683
|
+
},
|
|
1661
1684
|
constraints: {
|
|
1662
1685
|
type: "string",
|
|
1663
1686
|
description: "File constraints, e.g. '*.{ts,tsx} !test/' to filter files.",
|
|
@@ -1675,11 +1698,11 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1675
1698
|
},
|
|
1676
1699
|
|
|
1677
1700
|
async execute(
|
|
1678
|
-
|
|
1701
|
+
tid: string,
|
|
1679
1702
|
params: MultiGrepParams,
|
|
1680
1703
|
sig: AbortSignal | undefined,
|
|
1681
|
-
|
|
1682
|
-
|
|
1704
|
+
upd: unknown,
|
|
1705
|
+
ctx: ExtensionContext,
|
|
1683
1706
|
) {
|
|
1684
1707
|
if (sig?.aborted) return makeTextResult("Aborted", {});
|
|
1685
1708
|
|
|
@@ -1687,42 +1710,112 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1687
1710
|
return makeTextResult("Error: patterns array must have at least 1 element", { error: "empty patterns" });
|
|
1688
1711
|
}
|
|
1689
1712
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1713
|
+
const effectiveLimit = Math.max(1, params.limit ?? 100);
|
|
1714
|
+
const pattern = buildLiteralAlternationPattern(params.patterns);
|
|
1715
|
+
const requestedPath = trimToUndefined(params.path);
|
|
1716
|
+
const requestedConstraints = trimToUndefined(params.constraints);
|
|
1717
|
+
const effectivePath = requestedPath ?? getConstraintBackedPath(requestedConstraints);
|
|
1718
|
+
const hasNativeConstraints = Boolean(requestedPath || requestedConstraints);
|
|
1693
1719
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1720
|
+
if (_fffFinder && !_fffFinder.isDestroyed && !hasNativeConstraints) {
|
|
1721
|
+
try {
|
|
1722
|
+
const grepResult = _fffFinder.multiGrep({
|
|
1723
|
+
patterns: params.patterns,
|
|
1724
|
+
maxMatchesPerFile: Math.min(effectiveLimit, 50),
|
|
1725
|
+
smartCase: true,
|
|
1726
|
+
cursor: null,
|
|
1727
|
+
beforeContext: params.context ?? 0,
|
|
1728
|
+
afterContext: params.context ?? 0,
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
if (!grepResult.ok) {
|
|
1732
|
+
return makeTextResult(`multi_grep error: ${grepResult.error}`, { error: grepResult.error });
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
const grep: GrepResult = grepResult.value;
|
|
1736
|
+
const notices: string[] = [];
|
|
1737
|
+
if (_fffPartialIndex) notices.push("Warning: partial file index");
|
|
1738
|
+
if (grep.items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
|
|
1739
|
+
if (grep.nextCursor) {
|
|
1740
|
+
const cursorId = _cursorStore.store(grep.nextCursor);
|
|
1741
|
+
notices.push(`More results: cursor="${cursorId}"`);
|
|
1742
|
+
}
|
|
1706
1743
|
|
|
1707
|
-
|
|
1708
|
-
return makeTextResult(
|
|
1744
|
+
const textContent = appendNotices(fffFormatGrepText(grep.items, effectiveLimit), notices);
|
|
1745
|
+
return makeTextResult<GrepResultDetails>(textContent, {
|
|
1746
|
+
_type: "grepResult",
|
|
1747
|
+
text: textContent,
|
|
1748
|
+
pattern,
|
|
1749
|
+
matchCount: Math.min(grep.items.length, effectiveLimit),
|
|
1750
|
+
});
|
|
1751
|
+
} catch {
|
|
1752
|
+
/* fall through to SDK */
|
|
1709
1753
|
}
|
|
1754
|
+
}
|
|
1710
1755
|
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
const
|
|
1717
|
-
notices
|
|
1756
|
+
if (requestedConstraints || !multiGrepFallback) {
|
|
1757
|
+
try {
|
|
1758
|
+
const pathBackedConstraint = Boolean(
|
|
1759
|
+
requestedConstraints && !requestedPath && requestedConstraints === effectivePath,
|
|
1760
|
+
);
|
|
1761
|
+
const constraintsForRipgrep = pathBackedConstraint ? undefined : requestedConstraints;
|
|
1762
|
+
const notices: string[] = [];
|
|
1763
|
+
|
|
1764
|
+
if (!_fffFinder || _fffFinder.isDestroyed) notices.push("FFF unavailable, used ripgrep fallback");
|
|
1765
|
+
else if (hasNativeConstraints) notices.push("Used ripgrep fallback for constrained search");
|
|
1766
|
+
else notices.push("Used ripgrep fallback");
|
|
1767
|
+
|
|
1768
|
+
const rgResult = await multiGrepRipgrepFallback({
|
|
1769
|
+
cwd,
|
|
1770
|
+
patterns: params.patterns,
|
|
1771
|
+
path: effectivePath,
|
|
1772
|
+
constraints: constraintsForRipgrep,
|
|
1773
|
+
ignoreCase: shouldIgnoreCaseForPatterns(params.patterns),
|
|
1774
|
+
context: params.context,
|
|
1775
|
+
limit: effectiveLimit,
|
|
1776
|
+
signal: sig,
|
|
1777
|
+
});
|
|
1778
|
+
const textContent = normalizeLineEndings(rgResult.text) || "No matches found";
|
|
1779
|
+
if (rgResult.limitReached) notices.push(`${effectiveLimit} limit reached`);
|
|
1780
|
+
const finalText = appendNotices(textContent, notices);
|
|
1781
|
+
|
|
1782
|
+
return makeTextResult<GrepResultDetails>(finalText, {
|
|
1783
|
+
_type: "grepResult",
|
|
1784
|
+
text: finalText,
|
|
1785
|
+
pattern,
|
|
1786
|
+
matchCount: rgResult.matchCount,
|
|
1787
|
+
});
|
|
1788
|
+
} catch (error: unknown) {
|
|
1789
|
+
const message = getErrorMessage(error);
|
|
1790
|
+
return makeTextResult(`multi_grep error: ${message}`, { error: message });
|
|
1718
1791
|
}
|
|
1792
|
+
}
|
|
1719
1793
|
|
|
1720
|
-
|
|
1721
|
-
|
|
1794
|
+
try {
|
|
1795
|
+
const notices: string[] = [];
|
|
1796
|
+
if (!_fffFinder || _fffFinder.isDestroyed) notices.push("FFF unavailable, used SDK grep fallback");
|
|
1797
|
+
|
|
1798
|
+
const result = await multiGrepFallback.execute(
|
|
1799
|
+
tid,
|
|
1800
|
+
{
|
|
1801
|
+
pattern,
|
|
1802
|
+
path: effectivePath,
|
|
1803
|
+
ignoreCase: shouldIgnoreCaseForPatterns(params.patterns),
|
|
1804
|
+
context: params.context,
|
|
1805
|
+
limit: params.limit,
|
|
1806
|
+
},
|
|
1807
|
+
sig,
|
|
1808
|
+
upd as never,
|
|
1809
|
+
ctx,
|
|
1810
|
+
);
|
|
1811
|
+
const textContent = normalizeLineEndings(getTextContent(result)) || "No matches found";
|
|
1812
|
+
const finalText = appendNotices(textContent, notices);
|
|
1813
|
+
|
|
1814
|
+
return makeTextResult<GrepResultDetails>(finalText, {
|
|
1722
1815
|
_type: "grepResult",
|
|
1723
|
-
text:
|
|
1724
|
-
pattern
|
|
1725
|
-
matchCount:
|
|
1816
|
+
text: finalText,
|
|
1817
|
+
pattern,
|
|
1818
|
+
matchCount: textContent ? countRipgrepMatches(textContent) : 0,
|
|
1726
1819
|
});
|
|
1727
1820
|
} catch (error: unknown) {
|
|
1728
1821
|
const message = getErrorMessage(error);
|
|
@@ -1733,14 +1826,16 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1733
1826
|
renderCall(args: MultiGrepParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
1734
1827
|
resolveBaseBackground(theme);
|
|
1735
1828
|
const patterns = args.patterns ?? [];
|
|
1829
|
+
const path = args.path ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}` : "";
|
|
1736
1830
|
const constraints = args.constraints;
|
|
1737
1831
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1738
1832
|
let content =
|
|
1739
1833
|
theme.fg("toolTitle", theme.bold("multi_grep")) +
|
|
1740
1834
|
" " +
|
|
1741
1835
|
theme.fg("accent", patterns.map((p) => `"${p}"`).join(", "));
|
|
1836
|
+
content += path;
|
|
1742
1837
|
if (constraints) content += theme.fg("muted", ` (${constraints})`);
|
|
1743
|
-
text.setText(content);
|
|
1838
|
+
text.setText(fillToolBackground(content));
|
|
1744
1839
|
return text;
|
|
1745
1840
|
},
|
|
1746
1841
|
|