@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyhuynhgiabuu/pi-pretty",
3
- "version": "0.6.0",
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, BG_BASE, BG_ERROR, FG_DIM, RST } from "../config.js";
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 preview = lines.slice(0, maxShow).map(l => ` ${l}`).join("\n");
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;
@@ -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, BG_BASE, BG_ERROR, FG_DIM, RST } from "../config.js";
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 preview = lines.slice(0, maxShow).map(l => ` ${l}`).join("\n");
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 { renderFindResults, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
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 rendered = renderFindResults(d.content).split("\n").map(l => ` ${l}`).join("\n");
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