@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyhuynhgiabuu/pi-pretty",
3
- "version": "0.6.0",
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}`), rule(w)];
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, 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 {
@@ -57,9 +57,7 @@ export function registerReadTool(
57
57
  resolveBaseBackground(theme);
58
58
 
59
59
  const text = ctx.lastComponent ?? new TC("", 0, 0);
60
- const p = shortPath(cwd, home, String(args.path ?? ""));
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 rendered = renderFindResults(d.content).split("\n").map(l => ` ${l}`).join("\n");
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
 
@@ -211,7 +211,6 @@ describe("bash renderCall expansion", () => {
211
211
  expect(lines[3]).toMatch(/^ /);
212
212
  expect(lines[3].trim()).toBe("");
213
213
  expect(lines[4]).toMatch(/^ second error/);
214
- expect(lines[5]).toMatch(/^─+$/);
215
214
  });
216
215
  });
217
216