@heyhuynhgiabuu/pi-pretty 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/autocomplete.ts +96 -0
- package/src/index.ts +6 -0
- package/src/render.ts +0 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/grep.ts +28 -2
- package/src/tools/multi-grep.ts +27 -2
- package/src/tools/read.ts +39 -7
- package/test/bash-rendering.test.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyhuynhgiabuu/pi-pretty",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-pretty: FFF-backed @-mention autocomplete provider.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the built-in autocomplete provider and replaces the @-mention file
|
|
5
|
+
* suggestions with FFF frecency-ranked file search results.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
AutocompleteItem,
|
|
10
|
+
AutocompleteProvider,
|
|
11
|
+
AutocompleteSuggestions,
|
|
12
|
+
} from "@earendil-works/pi-tui";
|
|
13
|
+
import type { FileFinder } from "@ff-labs/fff-node";
|
|
14
|
+
|
|
15
|
+
/** How many @-mention suggestions to show at most. */
|
|
16
|
+
const MAX_SUGGESTIONS = 20;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract the query after a `@` character before the cursor.
|
|
20
|
+
* Returns the query string (empty string if bare `@`), or undefined if no `@` trigger.
|
|
21
|
+
*/
|
|
22
|
+
function extractAtPrefix(textBeforeCursor: string): string | undefined {
|
|
23
|
+
const match = textBeforeCursor.match(/(?:^|[ \t])@([^\s@]*)$/);
|
|
24
|
+
return match?.[1];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createFffAutocompleteProvider(
|
|
28
|
+
current: AutocompleteProvider,
|
|
29
|
+
getFinder: () => FileFinder | null,
|
|
30
|
+
): AutocompleteProvider {
|
|
31
|
+
return {
|
|
32
|
+
async getSuggestions(
|
|
33
|
+
lines: string[],
|
|
34
|
+
cursorLine: number,
|
|
35
|
+
cursorCol: number,
|
|
36
|
+
options: { signal: AbortSignal; force?: boolean },
|
|
37
|
+
): Promise<AutocompleteSuggestions | null> {
|
|
38
|
+
const currentLine = lines[cursorLine] ?? "";
|
|
39
|
+
const textBeforeCursor = currentLine.slice(0, cursorCol);
|
|
40
|
+
const query = extractAtPrefix(textBeforeCursor);
|
|
41
|
+
|
|
42
|
+
// Not an @-mention — delegate
|
|
43
|
+
if (query === undefined) {
|
|
44
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const finder = getFinder();
|
|
48
|
+
if (!finder) {
|
|
49
|
+
// FFF not available — delegate to built-in
|
|
50
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = finder.fileSearch(query, {
|
|
55
|
+
pageSize: MAX_SUGGESTIONS,
|
|
56
|
+
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (result.ok) {
|
|
60
|
+
const items: AutocompleteItem[] = result.value.items.map((item) => ({
|
|
61
|
+
value: item.relativePath,
|
|
62
|
+
label: item.fileName,
|
|
63
|
+
description: item.relativePath.slice(0, -(item.fileName.length + 1)),
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
if (items.length > 0) {
|
|
67
|
+
return { items, prefix: `@${query}` };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// FFF search failed — fall through to built-in
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// FFF returned no results or failed — let built-in handle it
|
|
75
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
applyCompletion(
|
|
79
|
+
lines: string[],
|
|
80
|
+
cursorLine: number,
|
|
81
|
+
cursorCol: number,
|
|
82
|
+
item: AutocompleteItem,
|
|
83
|
+
prefix: string,
|
|
84
|
+
): { lines: string[]; cursorLine: number; cursorCol: number } {
|
|
85
|
+
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
shouldTriggerFileCompletion(
|
|
89
|
+
lines: string[],
|
|
90
|
+
cursorLine: number,
|
|
91
|
+
cursorCol: number,
|
|
92
|
+
): boolean {
|
|
93
|
+
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { registerGrepTool } from "./tools/grep.js";
|
|
|
27
27
|
import { registerMultiGrepTool } from "./tools/multi-grep.js";
|
|
28
28
|
import { runMultiGrepRipgrepFallback } from "./multi-grep-fallback.js";
|
|
29
29
|
import { getDefaultAgentDir } from "./config.js";
|
|
30
|
+
import { createFffAutocompleteProvider } from "./autocomplete.js";
|
|
30
31
|
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
32
33
|
// Config
|
|
@@ -235,6 +236,11 @@ export default function piPrettyExtension(pi: ExtensionAPI, deps?: PiPrettyDeps)
|
|
|
235
236
|
} catch (error: unknown) {
|
|
236
237
|
ctx.ui?.notify?.(`FFF init failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
237
238
|
}
|
|
239
|
+
|
|
240
|
+
// Register FFF-backed @-mention autocomplete
|
|
241
|
+
ctx.ui?.addAutocompleteProvider?.((current) =>
|
|
242
|
+
createFffAutocompleteProvider(current, () => fffService?.getFinder() ?? null),
|
|
243
|
+
);
|
|
238
244
|
});
|
|
239
245
|
|
|
240
246
|
pi.on("session_shutdown", async () => {
|
package/src/render.ts
CHANGED
|
@@ -212,7 +212,6 @@ export async function renderFileContent(
|
|
|
212
212
|
out.push(`${lnum(ln, nw)} ${FG_RULE}│${RST} ${display}${RST}`);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
out.push(rule(tw));
|
|
216
215
|
if (total > maxLines) {
|
|
217
216
|
out.push(`${FG_DIM} … ${total - maxLines} more lines (${total} total)${RST}`);
|
|
218
217
|
}
|
package/src/tools/bash.ts
CHANGED
|
@@ -80,7 +80,7 @@ export function registerBashTool(
|
|
|
80
80
|
if (!output.trim()) return fillToolBackground(header, bg, w);
|
|
81
81
|
const max = ctx.expanded ? lineCount : MAX_PREVIEW_LINES;
|
|
82
82
|
const show = output.split("\n").slice(0, max);
|
|
83
|
-
const out = [header, rule(w), ...show.map((l: string) => ` ${l}`)
|
|
83
|
+
const out = [header, rule(w), ...show.map((l: string) => ` ${l}`)];
|
|
84
84
|
if (lineCount > max) out.push(`${FG_DIM} \u2026 ${lineCount - max} more lines${RST}`);
|
|
85
85
|
return fillToolBackground(out.join("\n"), bg, w);
|
|
86
86
|
};
|
package/src/tools/grep.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { type ToolDefinition, type ExtensionAPI, type ExtensionContext, type AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { SdkToolDef, GrepDetails, FffServiceWithCursor, TextContent, ThemeLike, RenderCtxLike } from "../types.js";
|
|
5
|
-
import { resolveBaseBackground, MAX_PREVIEW_LINES,
|
|
5
|
+
import { resolveBaseBackground, MAX_PREVIEW_LINES, BG_ERROR, FG_DIM, FG_LNUM, FG_RULE, RST } from "../config.js";
|
|
6
6
|
import { shortPath, normalizeLineEndings } from "../helpers.js";
|
|
7
7
|
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
8
8
|
import { renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
|
|
@@ -84,7 +84,33 @@ export function registerGrepTool(
|
|
|
84
84
|
if (d?._type === "grepResult" && d.text) {
|
|
85
85
|
const lines = d.text.split("\n");
|
|
86
86
|
const maxShow = ctx.expanded ? lines.length : Math.min(lines.length, MAX_PREVIEW_LINES);
|
|
87
|
-
const
|
|
87
|
+
const show = lines.slice(0, maxShow);
|
|
88
|
+
const nw = Math.max(3, 5);
|
|
89
|
+
|
|
90
|
+
// Build highlight regex from pattern
|
|
91
|
+
let hlRe: RegExp | null = null;
|
|
92
|
+
try { hlRe = new RegExp(`(${d.pattern})`, "gi"); } catch {}
|
|
93
|
+
|
|
94
|
+
const out: string[] = [];
|
|
95
|
+
let currentFile = "";
|
|
96
|
+
for (const line of show) {
|
|
97
|
+
const fileMatch = line.match(/^(.+?)[:-](\d+)[:-](.*)$/);
|
|
98
|
+
if (fileMatch) {
|
|
99
|
+
const [, file, lineNo, content] = fileMatch;
|
|
100
|
+
if (file !== currentFile) {
|
|
101
|
+
if (currentFile) out.push("");
|
|
102
|
+
out.push(` ${theme.fg("accent", theme.bold(file))}`);
|
|
103
|
+
currentFile = file;
|
|
104
|
+
}
|
|
105
|
+
let display = content;
|
|
106
|
+
if (hlRe) display = content.replace(hlRe, (m) => `${RST}${theme.fg("warning", theme.bold(m))}${RST}`);
|
|
107
|
+
const padded = `${FG_LNUM}${String(lineNo).padStart(nw)}${RST} ${FG_RULE}│${RST} ${display}${RST}`;
|
|
108
|
+
out.push(` ${padded}`);
|
|
109
|
+
} else if (line.trim()) {
|
|
110
|
+
out.push(` ${FG_DIM} ${line.trim()}${RST}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const preview = out.join("\n");
|
|
88
114
|
const more = lines.length > maxShow ? `\n${FG_DIM} ... ${lines.length - maxShow} more lines${RST}` : "";
|
|
89
115
|
text.setText(fillToolBackground(` ${FG_DIM}${d.matchCount} matches${RST}${renderToolMetrics(result)}\n${preview}${more}`));
|
|
90
116
|
return text;
|
package/src/tools/multi-grep.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { type ToolDefinition, type ExtensionAPI, type ExtensionContext, type AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import type { SdkToolDef, GrepDetails, FffServiceWithCursor, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
|
|
6
|
-
import { resolveBaseBackground, MAX_PREVIEW_LINES,
|
|
6
|
+
import { resolveBaseBackground, MAX_PREVIEW_LINES, BG_ERROR, FG_DIM, FG_LNUM, FG_RULE, RST } from "../config.js";
|
|
7
7
|
import { shortPath, normalizeLineEndings } from "../helpers.js";
|
|
8
8
|
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
9
9
|
import { renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
|
|
@@ -154,7 +154,32 @@ export function registerMultiGrepTool(
|
|
|
154
154
|
if (d?._type === "grepResult" && d.text) {
|
|
155
155
|
const lines = d.text.split("\n");
|
|
156
156
|
const maxShow = ctx.expanded ? lines.length : Math.min(lines.length, MAX_PREVIEW_LINES);
|
|
157
|
-
const
|
|
157
|
+
const show = lines.slice(0, maxShow);
|
|
158
|
+
const nw = Math.max(3, 5);
|
|
159
|
+
|
|
160
|
+
let hlRe: RegExp | null = null;
|
|
161
|
+
try { hlRe = new RegExp(`(${d.pattern})`, "gi"); } catch {}
|
|
162
|
+
|
|
163
|
+
const out: string[] = [];
|
|
164
|
+
let currentFile = "";
|
|
165
|
+
for (const line of show) {
|
|
166
|
+
const fileMatch = line.match(/^(.+?)[:-](\d+)[:-](.*)$/);
|
|
167
|
+
if (fileMatch) {
|
|
168
|
+
const [, file, lineNo, content] = fileMatch;
|
|
169
|
+
if (file !== currentFile) {
|
|
170
|
+
if (currentFile) out.push("");
|
|
171
|
+
out.push(` ${theme.fg("accent", theme.bold(file))}`);
|
|
172
|
+
currentFile = file;
|
|
173
|
+
}
|
|
174
|
+
let display = content;
|
|
175
|
+
if (hlRe) display = content.replace(hlRe, (m) => `${RST}${theme.fg("warning", theme.bold(m))}${RST}`);
|
|
176
|
+
const padded = `${FG_LNUM}${String(lineNo).padStart(nw)}${RST} ${FG_RULE}│${RST} ${display}${RST}`;
|
|
177
|
+
out.push(` ${padded}`);
|
|
178
|
+
} else if (line.trim()) {
|
|
179
|
+
out.push(` ${FG_DIM} ${line.trim()}${RST}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const preview = out.join("\n");
|
|
158
183
|
const more = lines.length > maxShow ? `\n${FG_DIM} ... ${lines.length - maxShow} more lines${RST}` : "";
|
|
159
184
|
text.setText(fillToolBackground(` ${FG_DIM}${d.matchCount} matches${RST}${renderToolMetrics(result)}\n${preview}${more}`));
|
|
160
185
|
return text;
|
package/src/tools/read.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { type ToolDefinition, type ExtensionAPI, type ExtensionContext, type AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { SdkToolDef, ReadDetails, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
|
|
5
|
-
import { resolveBaseBackground, termWidth, MAX_PREVIEW_LINES, BG_BASE, BG_ERROR, FG_DIM, RST } from "../config.js";
|
|
5
|
+
import { resolveBaseBackground, termWidth, MAX_PREVIEW_LINES, BG_BASE, BG_ERROR, FG_DIM, FG_LNUM, FG_RULE, RST } from "../config.js";
|
|
6
6
|
import { shortPath, normalizeLineEndings } from "../helpers.js";
|
|
7
7
|
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
8
|
-
import {
|
|
8
|
+
import { renderToolError, renderToolMetrics, fillToolBackground, renderFileContent } from "../render.js";
|
|
9
9
|
|
|
10
10
|
// Simple terminal image support check
|
|
11
11
|
function isImageTerminal(): boolean {
|
|
@@ -57,9 +57,7 @@ export function registerReadTool(
|
|
|
57
57
|
resolveBaseBackground(theme);
|
|
58
58
|
|
|
59
59
|
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
60
|
-
|
|
61
|
-
const off = typeof args.offset === "number" ? `:${args.offset}` : "";
|
|
62
|
-
text.setText(fillToolBackground(`\n ${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", p)}${theme.fg("dim", off)}`, ctx.isError ? BG_ERROR : undefined));
|
|
60
|
+
text.setText("");
|
|
63
61
|
return text;
|
|
64
62
|
},
|
|
65
63
|
|
|
@@ -92,11 +90,45 @@ export function registerReadTool(
|
|
|
92
90
|
return text;
|
|
93
91
|
}
|
|
94
92
|
|
|
95
|
-
// File content
|
|
93
|
+
// File content — line-numbered display
|
|
96
94
|
if (d?._type === "readFile" && d.content) {
|
|
97
|
-
const
|
|
95
|
+
const tw = termWidth();
|
|
96
|
+
const lines = d.content.split("\n");
|
|
97
|
+
const total = lines.length;
|
|
98
|
+
const maxShow = ctx.expanded ? lines.length : Math.min(lines.length, MAX_PREVIEW_LINES);
|
|
99
|
+
const show = lines.slice(0, maxShow);
|
|
100
|
+
const nw = Math.max(3, String(total).length);
|
|
101
|
+
const gw = nw + 3;
|
|
102
|
+
const cw = Math.max(1, tw - gw);
|
|
103
|
+
|
|
104
|
+
const p2 = shortPath(cwd, home, String(d.filePath ?? ""));
|
|
105
|
+
const off2 = typeof d.offset === "number" ? `:${d.offset}` : "";
|
|
106
|
+
const header = `${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", p2)}${theme.fg("dim", off2)}`;
|
|
107
|
+
const out: string[] = ["", ` ${header}`];
|
|
108
|
+
out.push(` ${FG_RULE}${"─".repeat(tw - 2)}${RST}`);
|
|
109
|
+
for (let i = 0; i < show.length; i++) {
|
|
110
|
+
const ln = (d.offset || 0) + i + 1;
|
|
111
|
+
const code = show[i] ?? "";
|
|
112
|
+
const display = code.length > cw ? code.slice(0, cw) + `${FG_DIM}›${RST}` : code;
|
|
113
|
+
const lineNo = String(ln);
|
|
114
|
+
out.push(` ${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${display}${RST}`);
|
|
115
|
+
}
|
|
116
|
+
if (total > maxShow) {
|
|
117
|
+
out.push(` ${FG_DIM} … ${total - maxShow} more lines (${total} total)${RST}`);
|
|
118
|
+
}
|
|
119
|
+
out.push("");
|
|
120
|
+
const rendered = out.join("\n");
|
|
98
121
|
text.setText(fillToolBackground(rendered));
|
|
99
122
|
(ctx as any).state._rt = rendered;
|
|
123
|
+
|
|
124
|
+
// Async syntax highlighting via Shiki
|
|
125
|
+
renderFileContent(d.content, d.filePath, d.offset || 0, maxShow, tw).then(hl => {
|
|
126
|
+
const padded = hl.split("\n").map(l => ` ${l}`).join("\n");
|
|
127
|
+
const rendered = `\n ${header}\n${padded}\n`;
|
|
128
|
+
text.setText(fillToolBackground(rendered));
|
|
129
|
+
(ctx as any).state._rt = rendered;
|
|
130
|
+
}).catch(() => {});
|
|
131
|
+
|
|
100
132
|
return text;
|
|
101
133
|
}
|
|
102
134
|
|