@heyhuynhgiabuu/pi-pretty 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/autocomplete.ts +96 -0
- package/src/index.ts +6 -0
- package/src/tools/grep.ts +28 -2
- package/src/tools/multi-grep.ts +27 -2
- package/src/tools/read.ts +34 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyhuynhgiabuu/pi-pretty",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
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/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 {
|
|
@@ -92,11 +92,41 @@ export function registerReadTool(
|
|
|
92
92
|
return text;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
// File content
|
|
95
|
+
// File content — line-numbered display
|
|
96
96
|
if (d?._type === "readFile" && d.content) {
|
|
97
|
-
const
|
|
97
|
+
const tw = termWidth();
|
|
98
|
+
const lines = d.content.split("\n");
|
|
99
|
+
const total = lines.length;
|
|
100
|
+
const maxShow = ctx.expanded ? lines.length : Math.min(lines.length, MAX_PREVIEW_LINES);
|
|
101
|
+
const show = lines.slice(0, maxShow);
|
|
102
|
+
const nw = Math.max(3, String(total).length);
|
|
103
|
+
const gw = nw + 3;
|
|
104
|
+
const cw = Math.max(1, tw - gw);
|
|
105
|
+
|
|
106
|
+
const out: string[] = [];
|
|
107
|
+
out.push(` ${FG_RULE}${"─".repeat(tw - 2)}${RST}`);
|
|
108
|
+
for (let i = 0; i < show.length; i++) {
|
|
109
|
+
const ln = (d.offset || 0) + i + 1;
|
|
110
|
+
const code = show[i] ?? "";
|
|
111
|
+
const display = code.length > cw ? code.slice(0, cw) + `${FG_DIM}›${RST}` : code;
|
|
112
|
+
const lineNo = String(ln);
|
|
113
|
+
out.push(` ${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${display}${RST}`);
|
|
114
|
+
}
|
|
115
|
+
out.push(` ${FG_RULE}${"─".repeat(tw - 2)}${RST}`);
|
|
116
|
+
if (total > maxShow) {
|
|
117
|
+
out.push(` ${FG_DIM} … ${total - maxShow} more lines (${total} total)${RST}`);
|
|
118
|
+
}
|
|
119
|
+
const rendered = out.join("\n");
|
|
98
120
|
text.setText(fillToolBackground(rendered));
|
|
99
121
|
(ctx as any).state._rt = rendered;
|
|
122
|
+
|
|
123
|
+
// Async syntax highlighting via Shiki
|
|
124
|
+
renderFileContent(d.content, d.filePath, d.offset || 0, maxShow, tw).then(hl => {
|
|
125
|
+
const padded = hl.split("\n").map(l => ` ${l}`).join("\n");
|
|
126
|
+
text.setText(fillToolBackground(padded));
|
|
127
|
+
(ctx as any).state._rt = padded;
|
|
128
|
+
}).catch(() => {});
|
|
129
|
+
|
|
100
130
|
return text;
|
|
101
131
|
}
|
|
102
132
|
|