@heyhuynhgiabuu/pi-pretty 0.6.16 → 0.6.18

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +6 -16
  3. package/dist/config.d.ts +1 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +61 -18
  6. package/dist/config.js.map +1 -1
  7. package/dist/expand.d.ts +0 -6
  8. package/dist/expand.d.ts.map +1 -1
  9. package/dist/expand.js +0 -13
  10. package/dist/expand.js.map +1 -1
  11. package/dist/image.js +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +18 -21
  15. package/dist/index.js.map +1 -1
  16. package/dist/render.d.ts +5 -7
  17. package/dist/render.d.ts.map +1 -1
  18. package/dist/render.js +16 -16
  19. package/dist/render.js.map +1 -1
  20. package/dist/tools/bash.d.ts +1 -1
  21. package/dist/tools/bash.d.ts.map +1 -1
  22. package/dist/tools/bash.js +18 -23
  23. package/dist/tools/bash.js.map +1 -1
  24. package/dist/tools/find.d.ts +2 -2
  25. package/dist/tools/find.d.ts.map +1 -1
  26. package/dist/tools/find.js +12 -8
  27. package/dist/tools/find.js.map +1 -1
  28. package/dist/tools/grep.d.ts +2 -2
  29. package/dist/tools/grep.d.ts.map +1 -1
  30. package/dist/tools/grep.js +5 -5
  31. package/dist/tools/grep.js.map +1 -1
  32. package/dist/tools/labels.d.ts +2 -0
  33. package/dist/tools/labels.d.ts.map +1 -0
  34. package/dist/tools/labels.js +7 -0
  35. package/dist/tools/labels.js.map +1 -0
  36. package/dist/tools/ls.d.ts +1 -1
  37. package/dist/tools/ls.d.ts.map +1 -1
  38. package/dist/tools/ls.js +1 -1
  39. package/dist/tools/ls.js.map +1 -1
  40. package/dist/tools/read.d.ts +1 -1
  41. package/dist/tools/read.d.ts.map +1 -1
  42. package/dist/tools/read.js +22 -40
  43. package/dist/tools/read.js.map +1 -1
  44. package/dist/tui-text.d.ts +2 -20
  45. package/dist/tui-text.d.ts.map +1 -1
  46. package/dist/tui-text.js +3 -46
  47. package/dist/tui-text.js.map +1 -1
  48. package/dist/types.d.ts +3 -27
  49. package/dist/types.d.ts.map +1 -1
  50. package/package.json +4 -4
  51. package/src/collapsed-hint.ts +1 -1
  52. package/src/config.ts +67 -22
  53. package/src/expand.ts +0 -14
  54. package/src/find-glob.ts +1 -1
  55. package/src/image.ts +1 -1
  56. package/src/index.ts +25 -34
  57. package/src/notices.ts +1 -1
  58. package/src/render.ts +44 -43
  59. package/src/tools/bash.ts +24 -48
  60. package/src/tools/find.ts +22 -36
  61. package/src/tools/grep.ts +12 -15
  62. package/src/tools/labels.ts +3 -0
  63. package/src/tools/ls.ts +16 -10
  64. package/src/tools/metrics.ts +1 -1
  65. package/src/tools/read.ts +39 -59
  66. package/src/tui-text.ts +10 -49
  67. package/src/types.ts +3 -35
  68. package/dist/multi-grep-fallback.d.ts +0 -30
  69. package/dist/multi-grep-fallback.d.ts.map +0 -1
  70. package/dist/multi-grep-fallback.js +0 -205
  71. package/dist/multi-grep-fallback.js.map +0 -1
  72. package/dist/tools/multi-grep.d.ts +0 -7
  73. package/dist/tools/multi-grep.d.ts.map +0 -1
  74. package/dist/tools/multi-grep.js +0 -298
  75. package/dist/tools/multi-grep.js.map +0 -1
  76. package/src/multi-grep-fallback.ts +0 -248
  77. package/src/tools/multi-grep.ts +0 -339
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * pi-pretty — Pretty terminal output for pi built-in tools.
3
3
  *
4
- * Enhances read, bash, ls, find, grep, multi_grep with:
4
+ * Enhances read, bash, ls, find, and grep with:
5
5
  * • Syntax-highlighted file content (Shiki)
6
6
  * • Colored bash exit status + output
7
7
  * • Tree-view directory listings with file-type icons
@@ -12,30 +12,28 @@
12
12
  // Re-export for tests
13
13
  export { __imageInternals } from "./image.js";
14
14
 
15
- import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
16
-
17
- import type { PiPrettyDeps } from "./types.js";
18
- import { getSharedFffService, type FffService } from "./fff.js";
19
- import { registerReadTool } from "./tools/read.js";
15
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
+ import { createFffAutocompleteProvider } from "./autocomplete.js";
17
+ import { getDefaultAgentDir } from "./config.js";
18
+ import { type FffService, getSharedFffService } from "./fff.js";
20
19
  import { registerBashTool } from "./tools/bash.js";
21
- import { registerLsTool } from "./tools/ls.js";
22
20
  import { registerFindTool } from "./tools/find.js";
23
21
  import { registerGrepTool } from "./tools/grep.js";
24
- import { registerMultiGrepTool } from "./tools/multi-grep.js";
25
- import { runMultiGrepRipgrepFallback } from "./multi-grep-fallback.js";
26
- import { getDefaultAgentDir } from "./config.js";
27
- import { createFffAutocompleteProvider } from "./autocomplete.js";
28
- import { registerDefaultCollapsedToolOutput } from "./expand.js";
22
+ import { registerLsTool } from "./tools/ls.js";
23
+ import { registerReadTool } from "./tools/read.js";
24
+ import type { PiPrettyDeps } from "./types.js";
29
25
 
30
26
  // ---------------------------------------------------------------------------
31
27
  // Config
32
28
  // ---------------------------------------------------------------------------
33
29
 
34
- function envDisabledTools(): Set<string> {
30
+ const DEFAULT_DISABLED_TOOLS = new Set(["ls"]);
31
+
32
+ function envTools(name: "PRETTY_DISABLE_TOOLS" | "PRETTY_ENABLE_TOOLS"): Set<string> {
35
33
  return new Set(
36
- (process.env.PRETTY_DISABLE_TOOLS ?? "")
34
+ (process.env[name] ?? "")
37
35
  .split(",")
38
- .map((s) => s.trim().toLowerCase())
36
+ .map((tool) => tool.trim().toLowerCase())
39
37
  .filter(Boolean),
40
38
  );
41
39
  }
@@ -47,9 +45,15 @@ function envDisabledTools(): Set<string> {
47
45
  export type { PiPrettyDeps };
48
46
 
49
47
  export default async function piPrettyExtension(pi: ExtensionAPI, deps?: PiPrettyDeps): Promise<void> {
50
- const disabledTools = envDisabledTools();
51
- const isToolEnabled = (name: string) => !disabledTools.has(name.toLowerCase());
52
- registerDefaultCollapsedToolOutput(pi);
48
+ const disabledTools = envTools("PRETTY_DISABLE_TOOLS");
49
+ const enabledTools = envTools("PRETTY_ENABLE_TOOLS");
50
+ const isToolEnabled = (name: string) => {
51
+ const normalizedName = name.toLowerCase();
52
+ return (
53
+ !disabledTools.has(normalizedName) &&
54
+ (!DEFAULT_DISABLED_TOOLS.has(normalizedName) || enabledTools.has(normalizedName))
55
+ );
56
+ };
53
57
  const cwd = process.cwd();
54
58
 
55
59
  // ------------------------------------------------------------------
@@ -62,10 +66,7 @@ export default async function piPrettyExtension(pi: ExtensionAPI, deps?: PiPrett
62
66
 
63
67
  const maybeGetAgentDir = deps?.sdk?.getAgentDir;
64
68
  const agentDir = typeof maybeGetAgentDir === "function" ? maybeGetAgentDir() : getDefaultAgentDir();
65
- let fffService: FffService | null = getSharedFffService(deps?.fffModule, agentDir);
66
-
67
- // Ripgrep fallback for multi_grep
68
- const multiGrepFallback = deps?.multiGrepRipgrepFallback ?? runMultiGrepRipgrepFallback;
69
+ const fffService: FffService | null = getSharedFffService(deps?.fffModule, agentDir);
69
70
 
70
71
  // Text component for custom rendering (DI-friendly)
71
72
  const TextComp = deps?.TextComponent;
@@ -78,8 +79,8 @@ export default async function piPrettyExtension(pi: ExtensionAPI, deps?: PiPrett
78
79
  pi.registerCommand("fff-health", {
79
80
  description: "Show FFF file finder health and indexer status",
80
81
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
81
- const fff = fffService!;
82
- if (!fff.isAvailable) {
82
+ const fff = fffService;
83
+ if (!fff || !fff.isAvailable) {
83
84
  ctx.ui.notify("FFF not initialized", "warning");
84
85
  return;
85
86
  }
@@ -229,16 +230,6 @@ export default async function piPrettyExtension(pi: ExtensionAPI, deps?: PiPrett
229
230
  if (isToolEnabled("grep") && createGrepTool) {
230
231
  registerGrepTool(pi, cwd, fffService, createGrepTool(cwd), TextComp);
231
232
  }
232
- if (isToolEnabled("multi_grep") && (fffService || createGrepTool)) {
233
- registerMultiGrepTool(
234
- pi,
235
- cwd,
236
- fffService,
237
- createGrepTool ? createGrepTool(cwd) : undefined,
238
- multiGrepFallback,
239
- TextComp,
240
- );
241
- }
242
233
 
243
234
  // Fallback padding for SDK-rendered tool bodies. The SDK reads
244
235
  // result.content[0].text and slices collapsed output to roughly the first
package/src/notices.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  /** Stable user-facing strings for FFF/grep tool footers (not source-line titles). */
2
2
 
3
- export const NOTICE_PARTIAL_FILE_INDEX = "FFF index incomplete; results may be missing files";
3
+ export const NOTICE_PARTIAL_FILE_INDEX = "FFF index incomplete; results may be missing files";
package/src/render.ts CHANGED
@@ -5,49 +5,47 @@
5
5
  * They are async only when Shiki syntax highlighting is involved.
6
6
  */
7
7
 
8
- import type { BundledLanguage } from "shiki";
9
- import { codeToANSI } from "@shikijs/cli";
10
8
  import { basename, dirname } from "node:path";
11
-
9
+ import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
10
+ // Top-level value import so jiti's pi-tui alias applies (it only rewrites
11
+ // static top-level import/require, not function-body require — see tui-text.ts).
12
+ import { Text as TuiText, truncateToWidth } from "@earendil-works/pi-tui";
13
+ import { codeToANSI } from "@shikijs/cli";
14
+ import type { BundledLanguage } from "shiki";
12
15
  import {
13
- TOOL_RESULT_INDENT,
14
- RST,
15
- FG_LNUM,
16
+ BG_BASE,
17
+ BG_ERROR,
18
+ CACHE_LIMIT,
19
+ detectLang,
20
+ dirIcon,
21
+ FG_BLUE,
16
22
  FG_DIM,
17
- FG_RULE,
18
23
  FG_GREEN,
24
+ FG_LNUM,
19
25
  FG_RED,
26
+ FG_RULE,
20
27
  FG_YELLOW,
21
- FG_BLUE,
22
- BG_BASE,
23
- BG_ERROR,
24
- dirIcon,
25
- detectLang,
26
- termWidth,
27
- MAX_PREVIEW_LINES,
28
28
  MAX_HL_CHARS,
29
- CACHE_LIMIT,
29
+ MAX_PREVIEW_LINES,
30
+ RST,
30
31
  resolveBaseBackground,
32
+ TOOL_RESULT_INDENT,
33
+ termWidth,
31
34
  } from "./config.js";
32
35
  import {
33
- normalizeLineEndings,
34
- humanSize,
35
- formatElapsedMs,
36
- formatCharCount,
37
- ELAPSED_KEY,
38
36
  CHARS_KEY,
39
37
  compactErrorLines,
38
+ ELAPSED_KEY,
39
+ formatCharCount,
40
+ formatElapsedMs,
41
+ humanSize,
42
+ normalizeLineEndings,
40
43
  } from "./helpers.js";
41
- import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
42
- import type { ThemeLike, RenderCtxLike as RenderContext } from "./types.js";
43
-
44
- // ---------------------------------------------------------------------------
45
- // Lazy imports — avoid top-level require() that blocks module loading
46
- // ---------------------------------------------------------------------------
44
+ import type { RenderCtxLike as RenderContext, ThemeLike } from "./types.js";
47
45
 
48
- /** Lazy accessor to pi-tui's truncateToWidth */
46
+ /** Thin wrapper over pi-tui's truncateToWidth (imported at top level above). */
49
47
  function _truncateToWidth(text: string, maxWidth: number, ellipsis?: string, pad?: boolean): string {
50
- return require("@earendil-works/pi-tui").truncateToWidth(text, maxWidth, ellipsis, pad);
48
+ return truncateToWidth(text, maxWidth, ellipsis, pad);
51
49
  }
52
50
 
53
51
  // ---------------------------------------------------------------------------
@@ -73,7 +71,7 @@ function resolveTheme(): BundledTheme {
73
71
  }
74
72
  }
75
73
 
76
- let THEME: BundledTheme = resolveTheme();
74
+ const THEME: BundledTheme = resolveTheme();
77
75
  const _cache = new Map<string, string[]>();
78
76
 
79
77
  function _touch(k: string, v: string[]): string[] {
@@ -174,8 +172,7 @@ export function fillToolBackground(text: string, bg = BG_BASE, width?: number):
174
172
  return bg ? bg + stripped : stripped;
175
173
  }
176
174
  const plainLead = line.replace(/\x1b\[[0-9;]*m/g, "");
177
- const skipPad =
178
- line.startsWith(TOOL_RESULT_INDENT) || plainLead.startsWith(TOOL_RESULT_INDENT);
175
+ const skipPad = line.startsWith(TOOL_RESULT_INDENT) || plainLead.startsWith(TOOL_RESULT_INDENT);
179
176
  const fitted = _truncateToWidth(line, width, "", !skipPad);
180
177
  const stripped = preserveBoxBackground(fitted);
181
178
  return bg ? bg + stripped : stripped;
@@ -205,6 +202,11 @@ export function renderToolMetrics(result: AgentToolResult<Record<string, unknown
205
202
  return `${FG_DIM}· ${[elapsed, chars].filter(Boolean).join(" · ")}${RST}`;
206
203
  }
207
204
 
205
+ export function renderToolDuration(result: AgentToolResult<Record<string, unknown>>): string {
206
+ const details = result.details as Record<string, unknown> | undefined;
207
+ return formatElapsedMs(details?.[ELAPSED_KEY] as number | undefined);
208
+ }
209
+
208
210
  // ---------------------------------------------------------------------------
209
211
  // Error renderer
210
212
  // ---------------------------------------------------------------------------
@@ -246,14 +248,7 @@ export async function renderFileContent(
246
248
  // Bash — colored exit status
247
249
  // ---------------------------------------------------------------------------
248
250
 
249
- export function renderBashOutput(text: string, exitCode: number | null): { summary: string; body: string } {
250
- const isOk = exitCode === 0;
251
- const statusIcon = isOk ? "✓" : "✗";
252
- const codeStr =
253
- exitCode !== null
254
- ? `${isOk ? FG_GREEN : FG_RED}${statusIcon} exit ${exitCode}${RST}`
255
- : `${FG_YELLOW}⚡ killed${RST}`;
256
-
251
+ export function renderBashOutput(text: string): string {
257
252
  const lines = text.split("\n");
258
253
  const maxShow = MAX_PREVIEW_LINES;
259
254
  const show = lines.slice(0, maxShow);
@@ -262,7 +257,7 @@ export function renderBashOutput(text: string, exitCode: number | null): { summa
262
257
  let body = show.join("\n");
263
258
  if (remaining > 0) body += `\n${TOOL_RESULT_INDENT}${FG_DIM}… ${remaining} more lines${RST}`;
264
259
 
265
- return { summary: codeStr, body };
260
+ return body;
266
261
  }
267
262
 
268
263
  // ---------------------------------------------------------------------------
@@ -391,7 +386,7 @@ export async function renderGrepResults(text: string, pattern: string): Promise<
391
386
  export function makeRenderCall(toolName: string) {
392
387
  return (args: Record<string, unknown>, theme: ThemeLike, ctx: RenderContext) => {
393
388
  resolveBaseBackground(theme);
394
- const text = ctx.lastComponent ?? new (require("@earendil-works/pi-tui").Text)("", 0, 0);
389
+ const text = ctx.lastComponent ?? new TuiText("", 0, 0);
395
390
  const bg = ctx.isError ? BG_ERROR : undefined;
396
391
  text.setText(fillToolBackground(`${theme.fg("toolTitle", theme.bold(toolName))}`, bg));
397
392
  return text;
@@ -401,7 +396,7 @@ export function makeRenderCall(toolName: string) {
401
396
  export function makeRenderResult() {
402
397
  return (result: AgentToolResult<Record<string, unknown>>, _opt: unknown, theme: ThemeLike, ctx: RenderContext) => {
403
398
  resolveBaseBackground(theme);
404
- const text = ctx.lastComponent ?? new (require("@earendil-works/pi-tui").Text)("", 0, 0);
399
+ const text = ctx.lastComponent ?? new TuiText("", 0, 0);
405
400
  if (ctx.isError) {
406
401
  text.setText(renderToolError(getTextContent(result) || "Error", theme));
407
402
  return text;
@@ -414,7 +409,13 @@ export function makeRenderResult() {
414
409
  const preview = lines.slice(0, maxShow).join("\n");
415
410
  const more = lines.length > maxShow ? `\n${FG_DIM}... ${lines.length - maxShow} more lines${RST}` : "";
416
411
  const metrics = renderToolMetrics(result);
417
- text.setText(fillToolBackground(`${TOOL_RESULT_INDENT}${preview}${more}${metrics ? `\n${TOOL_RESULT_INDENT}${metrics}` : ""}`, undefined, renderWidth));
412
+ text.setText(
413
+ fillToolBackground(
414
+ `${TOOL_RESULT_INDENT}${preview}${more}${metrics ? `\n${TOOL_RESULT_INDENT}${metrics}` : ""}`,
415
+ undefined,
416
+ renderWidth,
417
+ ),
418
+ );
418
419
  } else {
419
420
  text.setText(fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", "(no text output)")}`));
420
421
  }
package/src/tools/bash.ts CHANGED
@@ -1,28 +1,12 @@
1
1
  /* pi-pretty: bash tool -- command execution with styled output. */
2
2
 
3
- import {
4
- type ToolDefinition,
5
- type ExtensionAPI,
6
- type ExtensionContext,
7
- type AgentToolResult,
8
- } from "@earendil-works/pi-coding-agent";
9
- import type { SdkToolDef, BashDetails, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
10
- import {
11
- TOOL_RESULT_INDENT,
12
- resolveBaseBackground,
13
- termWidth,
14
- MAX_PREVIEW_LINES,
15
- BG_BASE,
16
- BG_ERROR,
17
- FG_DIM,
18
- FG_RULE,
19
- RST,
20
- } from "../config.js";
21
- import { wrapExecuteWithMetrics } from "./metrics.js";
22
- import { collapsedExpandFooter } from "../collapsed-hint.js";
23
- import { renderBashOutput, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
3
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
4
+ import { BG_BASE, FG_DIM, MAX_PREVIEW_LINES, resolveBaseBackground, TOOL_RESULT_INDENT, termWidth } from "../config.js";
5
+ import { compactErrorLines, inferBashExitCode, stripBashExitStatusLine } from "../helpers.js";
6
+ import { fillToolBackground, renderToolDuration, renderToolError } from "../render.js";
24
7
  import { resolveTextCtor } from "../tui-text.js";
25
- import { stripBashExitStatusLine, inferBashExitCode, compactErrorLines } from "../helpers.js";
8
+ import type { BashDetails, ComponentLike, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
9
+ import { wrapExecuteWithMetrics } from "./metrics.js";
26
10
 
27
11
  type Result = AgentToolResult<Record<string, unknown>>;
28
12
 
@@ -40,8 +24,8 @@ export function registerBashTool(
40
24
  label: "Bash",
41
25
  description: sdkTool.description
42
26
  ? `${sdkTool.description} For text search: \`rg -n\`.`
43
- : "Execute shell commands. For text search: \`rg -n\`.",
44
- promptSnippet: "Execute commands via bash. For text search: \`rg -n\`.",
27
+ : "Execute shell commands. For text search: `rg -n`.",
28
+ promptSnippet: "Execute commands via bash. For text search: `rg -n`.",
45
29
  promptGuidelines: [
46
30
  "For text search: `rg -n`. If no results, try `rg -u` (respects .gitignore by default).",
47
31
  "In rg: | means alternation, \\| means literal pipe. Opposite of GNU grep. Never use \\| for alternation.",
@@ -78,14 +62,11 @@ export function registerBashTool(
78
62
  rawCmd.length === 0
79
63
  ? theme.fg("toolOutput", "...")
80
64
  : !ctx.expanded && rawCmd.length > headerBudget
81
- ? rawCmd.slice(0, Math.max(1, headerBudget)) + "…"
65
+ ? `${rawCmd.slice(0, Math.max(1, headerBudget))}…`
82
66
  : rawCmd;
67
+ const commandLabel = theme.fg(ctx.isError ? "error" : "toolTitle", theme.bold(`$ ${cmd}`));
83
68
  text.setText(
84
- fillToolBackground(
85
- `\n${TOOL_RESULT_INDENT}${theme.fg("toolTitle", theme.bold(`$ ${cmd}`))}${t}`,
86
- ctx.isError ? BG_ERROR : undefined,
87
- ctx.expanded ? undefined : tw,
88
- ),
69
+ fillToolBackground(`\n${TOOL_RESULT_INDENT}${commandLabel}${t}`, undefined, ctx.expanded ? undefined : tw),
89
70
  );
90
71
  return text;
91
72
  },
@@ -111,26 +92,22 @@ export function registerBashTool(
111
92
 
112
93
  if (d?._type === "bashResult") {
113
94
  const isErr = ctx.isError || (d.exitCode !== null && d.exitCode !== 0);
114
- const bg = isErr ? BG_ERROR : undefined;
115
95
  const cleaned = stripBashExitStatusLine(d.text);
116
96
  const output = isErr ? compactErrorLines(cleaned).join("\n") : cleaned;
117
- const { summary } = renderBashOutput(output, d.exitCode);
118
97
  const lineCount = output.split("\n").length;
119
- const info =
120
- lineCount > 1
121
- ? `${TOOL_RESULT_INDENT}${FG_DIM}(${lineCount} lines)${RST} ${renderToolMetrics(result)}`
122
- : ` ${renderToolMetrics(result)}`;
123
- const header = `${TOOL_RESULT_INDENT}${summary}${info}`;
98
+ const info = [`${lineCount} lines`, renderToolDuration(result), !ctx.expanded ? "ctrl+o to expand" : ""]
99
+ .filter(Boolean)
100
+ .map((part) => theme.fg("dim", part))
101
+ .join(theme.fg("dim", " · "));
102
+ const header = `${TOOL_RESULT_INDENT}${info}`;
124
103
  const rw = termWidth();
125
104
 
126
105
  const renderFn = (w: number) => {
127
- if (!ctx.expanded) {
128
- return fillToolBackground([header, collapsedExpandFooter()].join("\n"), bg, w);
129
- }
130
- if (!output.trim()) return fillToolBackground(header, bg, w);
106
+ if (!ctx.expanded) return fillToolBackground(`${header}\n`, undefined, w);
107
+ if (!output.trim()) return fillToolBackground(`${header}\n`, undefined, w);
131
108
  const show = output.split("\n");
132
- const out = [header, rule(w), ...show.map((l: string) => `${TOOL_RESULT_INDENT}${l}`)];
133
- return fillToolBackground(out.join("\n"), bg, w);
109
+ const out = [header, "", ...show.map((line: string) => `${TOOL_RESULT_INDENT}${line}`)];
110
+ return fillToolBackground(`${out.join("\n")}\n`, undefined, w);
134
111
  };
135
112
 
136
113
  text.setText(renderFn(rw));
@@ -140,7 +117,7 @@ export function registerBashTool(
140
117
  let key: string | undefined;
141
118
  (text as unknown as Record<string, unknown>).render = (w: number) => {
142
119
  const width = Math.max(1, Math.floor(w || termWidth()));
143
- const k = `bash:${ctx.expanded ? "1" : "0"}:${width}:${d.exitCode ?? "killed"}:${output.length}:${renderToolMetrics(result)}`;
120
+ const k = `bash:${ctx.expanded ? "1" : "0"}:${width}:${d.exitCode ?? "killed"}:${output.length}:${renderToolDuration(result)}`;
144
121
  if (key !== k) {
145
122
  text.setText(renderFn(width));
146
123
  key = k;
@@ -157,16 +134,15 @@ export function registerBashTool(
157
134
  }
158
135
  const fc = result.content?.[0];
159
136
  text.setText(
160
- fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`),
137
+ fillToolBackground(
138
+ `${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
139
+ ),
161
140
  );
162
141
  return text;
163
142
  },
164
143
  } as unknown as ToolDefinition<any, any, any>);
165
144
  }
166
145
 
167
- function rule(w: number): string {
168
- return `${FG_RULE}${"\u2500".repeat(w)}${RST}`;
169
- }
170
146
  function getText(result: Result): string {
171
147
  return (
172
148
  ((result.content ?? []) as TextContent[])
package/src/tools/find.ts CHANGED
@@ -1,20 +1,15 @@
1
1
  /* pi-pretty: find tool -- FFF-backed file search with SDK (fd) fallback. */
2
2
 
3
3
  import { isAbsolute, relative } from "node:path";
4
- import {
5
- type ToolDefinition,
6
- type ExtensionAPI,
7
- type ExtensionContext,
8
- type AgentToolResult,
9
- } from "@earendil-works/pi-coding-agent";
10
- import type { SdkToolDef, FindDetails, FffServiceWithCursor, TextContent, ThemeLike, RenderCtxLike } from "../types.js";
11
- import { TOOL_RESULT_INDENT, BG_ERROR, FG_DIM, RST, resolveBaseBackground } from "../config.js";
4
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
5
+ import { BG_ERROR, FG_DIM, RST, resolveBaseBackground, TOOL_RESULT_INDENT } from "../config.js";
6
+ import { isLikelyGlobPattern, normalizeFindGlobPattern } from "../find-glob.js";
12
7
  import { shortPath } from "../helpers.js";
13
- import { wrapExecuteWithMetrics } from "./metrics.js";
14
- import { renderFindResults, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
15
- import { resolveTextCtor } from "../tui-text.js";
16
8
  import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
17
- import { isLikelyGlobPattern, normalizeFindGlobPattern } from "../find-glob.js";
9
+ import { fillToolBackground, renderFindResults, renderToolDuration, renderToolError } from "../render.js";
10
+ import { resolveTextCtor } from "../tui-text.js";
11
+ import type { FffServiceWithCursor, FindDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
12
+ import { wrapExecuteWithMetrics } from "./metrics.js";
18
13
 
19
14
  type Result = AgentToolResult<Record<string, unknown>>;
20
15
 
@@ -105,7 +100,9 @@ export function registerFindTool(
105
100
  const basePathResult = fff.getBasePath();
106
101
  const basePath = basePathResult.ok ? basePathResult.value : null;
107
102
  const globPattern = buildGlobPattern(pattern, path, basePath);
108
- const searchResult = fff.glob(globPattern, { pageSize: effectiveLimit });
103
+ const searchResult = fff.glob(globPattern, {
104
+ pageSize: effectiveLimit,
105
+ });
109
106
 
110
107
  if (searchResult.ok) {
111
108
  const items = searchResult.value.items.slice(0, effectiveLimit);
@@ -153,22 +150,15 @@ export function registerFindTool(
153
150
  const a = args as { pattern?: unknown; path?: unknown; limit?: unknown };
154
151
  const text = (ctx as RenderCtxLike).lastComponent ?? new TC("", 0, 0);
155
152
  const pattern = a.pattern == null ? "" : String(a.pattern);
156
- const pathArg =
157
- a.path == null ? "<missing>" : shortPath(cwd, home, String(a.path));
153
+ const pathArg = a.path == null ? "<missing>" : shortPath(cwd, home, String(a.path));
158
154
  const limit = a.limit;
159
- const findLabel = theme.fg("toolTitle", theme.bold("find"));
160
- const patternPart = pattern ? theme.fg("accent", pattern) : "";
155
+ const findLabel = theme.fg(ctx.isError ? "error" : "toolTitle", theme.bold("✱ find"));
156
+ const patternPart = pattern ? theme.fg("toolTitle", pattern) : "";
161
157
  const inPart = theme.fg("dim", " in ");
162
158
  const pathPart = theme.fg("toolOutput", pathArg);
163
- const limitPart =
164
- limit !== undefined && limit !== null ? theme.fg("dim", ` limit ${limit}`) : "";
159
+ const limitPart = limit !== undefined && limit !== null ? theme.fg("dim", ` limit ${limit}`) : "";
165
160
  const out = `${findLabel} ${patternPart}${inPart}${pathPart}${limitPart}`;
166
- text.setText(
167
- fillToolBackground(
168
- `\n${TOOL_RESULT_INDENT}${out}`,
169
- ctx.isError ? BG_ERROR : undefined,
170
- ),
171
- );
161
+ text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${out}`, ctx.isError ? BG_ERROR : undefined));
172
162
  return text;
173
163
  },
174
164
 
@@ -186,17 +176,14 @@ export function registerFindTool(
186
176
  const noticeStr = d.notices?.length
187
177
  ? `\n${TOOL_RESULT_INDENT}${theme.fg("warning", `[${d.notices.join(". ")}]`)}`
188
178
  : "";
189
- text.setText(
190
- fillToolBackground(
191
- `\n${TOOL_RESULT_INDENT}${theme.fg("dim", "0 files")}${noticeStr}\n`,
192
- ),
193
- );
179
+ text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${theme.fg("dim", "0 files")}${noticeStr}\n`));
194
180
  return text;
195
181
  }
196
182
  if (!ctx.expanded) {
183
+ const duration = renderToolDuration(r);
197
184
  text.setText(
198
185
  fillToolBackground(
199
- `${TOOL_RESULT_INDENT}${FG_DIM}${d.matchCount} files — ctrl+o to expand${RST}${renderToolMetrics(r)}\n`,
186
+ `${TOOL_RESULT_INDENT}${FG_DIM}${d.matchCount} files — ctrl+o to expand${RST}${duration ? `${FG_DIM}· ${duration}${RST}` : ""}\n`,
200
187
  ),
201
188
  );
202
189
  return text;
@@ -208,20 +195,19 @@ export function registerFindTool(
208
195
  const noticeStr = d.notices?.length
209
196
  ? `\n${TOOL_RESULT_INDENT}${theme.fg("warning", `[${d.notices.join(". ")}]`)}`
210
197
  : "";
198
+ const duration = renderToolDuration(r);
211
199
  text.setText(
212
200
  fillToolBackground(
213
- `\n${TOOL_RESULT_INDENT}${theme.fg("dim", `${d.matchCount} files`)}${renderToolMetrics(r)}\n${rendered}${noticeStr}\n`,
201
+ `\n${TOOL_RESULT_INDENT}${theme.fg("dim", `${d.matchCount} files`)}${duration ? `${FG_DIM}· ${duration}${RST}` : ""}\n${rendered}${noticeStr}\n`,
214
202
  ),
215
203
  );
216
204
  return text;
217
205
  }
218
206
  const fc = r.content?.[0] as TextContent | undefined;
219
207
  text.setText(
220
- fillToolBackground(
221
- `\n${TOOL_RESULT_INDENT}${theme.fg("dim", fc?.text?.slice(0, 120) ?? "0 files")}\n`,
222
- ),
208
+ fillToolBackground(`\n${TOOL_RESULT_INDENT}${theme.fg("dim", fc?.text?.slice(0, 120) ?? "0 files")}\n`),
223
209
  );
224
210
  return text;
225
211
  },
226
212
  } as unknown as ToolDefinition);
227
- }
213
+ }
package/src/tools/grep.ts CHANGED
@@ -1,19 +1,14 @@
1
1
  /* pi-pretty: grep tool -- FFF-backed text search with SDK fallback. */
2
2
 
3
- import {
4
- type ToolDefinition,
5
- type ExtensionAPI,
6
- type ExtensionContext,
7
- type AgentToolResult,
8
- } from "@earendil-works/pi-coding-agent";
9
- import type { SdkToolDef, GrepDetails, FffServiceWithCursor, TextContent, ThemeLike, RenderCtxLike } from "../types.js";
10
- import { TOOL_RESULT_INDENT, MAX_PREVIEW_LINES, BG_ERROR, FG_DIM, RST, resolveBaseBackground } from "../config.js";
11
- import { shortPath, normalizeLineEndings } from "../helpers.js";
12
- import { wrapExecuteWithMetrics } from "./metrics.js";
13
- import { renderToolError, fillToolBackground } from "../render.js";
14
- import { resolveTextCtor } from "../tui-text.js";
3
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
4
+ import { BG_ERROR, FG_DIM, MAX_PREVIEW_LINES, RST, resolveBaseBackground, TOOL_RESULT_INDENT } from "../config.js";
15
5
  import { fffFormatGrepText } from "../fff-helpers.js";
6
+ import { normalizeLineEndings, shortPath } from "../helpers.js";
16
7
  import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
8
+ import { fillToolBackground, renderToolError } from "../render.js";
9
+ import { resolveTextCtor } from "../tui-text.js";
10
+ import type { FffServiceWithCursor, GrepDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
11
+ import { wrapExecuteWithMetrics } from "./metrics.js";
17
12
 
18
13
  const invalidArg = "<missing>";
19
14
 
@@ -111,7 +106,7 @@ export function registerGrepTool(
111
106
  const limit = args.limit;
112
107
  const literal = args.literal === true;
113
108
  const caseInsensitive = args.caseInsensitive === true || args.ignoreCase === true;
114
- let out = `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", `/${pattern || ""}/`)}${theme.fg("toolOutput", ` in ${path}`)}`;
109
+ let out = `${theme.fg(ctx.isError ? "error" : "toolTitle", theme.bold("✱ grep"))} ${theme.fg("toolTitle", `/${pattern || ""}/`)}${theme.fg("toolOutput", ` in ${path}`)}`;
115
110
  if (glob) out += theme.fg("dim", ` (${String(glob)})`);
116
111
  if (limit !== undefined && limit !== null) out += theme.fg("dim", ` limit ${limit}`);
117
112
  if (literal) out += theme.fg("dim", ` (literal)`);
@@ -158,13 +153,15 @@ export function registerGrepTool(
158
153
  if (remaining > 0) {
159
154
  out.push(theme.fg("muted", `… (${remaining} more ${remaining === 1 ? "line" : "lines"}, to expand)`));
160
155
  }
161
- const body = out.map((l) => `${TOOL_RESULT_INDENT}${l}`).join("\n") + "\n\n";
156
+ const body = `${out.map((l) => `${TOOL_RESULT_INDENT}${l}`).join("\n")}\n\n`;
162
157
  text.setText(fillToolBackground(body, ctx.isError ? BG_ERROR : undefined));
163
158
  return text;
164
159
  }
165
160
  const fc = result.content?.[0];
166
161
  const fallback = fc && "text" in fc ? String(fc.text).slice(0, 120) : "no matches";
167
- text.setText(fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", fallback)}`, ctx.isError ? BG_ERROR : undefined));
162
+ text.setText(
163
+ fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", fallback)}`, ctx.isError ? BG_ERROR : undefined),
164
+ );
168
165
  return text;
169
166
  },
170
167
  } as unknown as ToolDefinition<any, any, any>);
@@ -0,0 +1,3 @@
1
+ export function customToolTitle(name: string): string {
2
+ return `⚙ ${name}`;
3
+ }
package/src/tools/ls.ts CHANGED
@@ -1,17 +1,21 @@
1
1
  /* pi-pretty: ls tool -- directory listing with styled output. */
2
2
 
3
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
3
4
  import {
4
- type ToolDefinition,
5
- type ExtensionAPI,
6
- type ExtensionContext,
7
- type AgentToolResult,
8
- } from "@earendil-works/pi-coding-agent";
9
- import type { SdkToolDef, LsDetails, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
10
- import { TOOL_RESULT_INDENT, resolveBaseBackground, termWidth, MAX_PREVIEW_LINES, BG_BASE, BG_ERROR, FG_DIM, RST } from "../config.js";
5
+ BG_BASE,
6
+ BG_ERROR,
7
+ FG_DIM,
8
+ MAX_PREVIEW_LINES,
9
+ RST,
10
+ resolveBaseBackground,
11
+ TOOL_RESULT_INDENT,
12
+ termWidth,
13
+ } from "../config.js";
11
14
  import { shortPath } from "../helpers.js";
12
- import { wrapExecuteWithMetrics } from "./metrics.js";
13
- import { renderTree, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
15
+ import { fillToolBackground, renderToolError, renderToolMetrics, renderTree } from "../render.js";
14
16
  import { resolveTextCtor } from "../tui-text.js";
17
+ import type { ComponentLike, LsDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
18
+ import { wrapExecuteWithMetrics } from "./metrics.js";
15
19
 
16
20
  type Result = AgentToolResult<Record<string, unknown>>;
17
21
 
@@ -91,7 +95,9 @@ export function registerLsTool(
91
95
  }
92
96
  const fc = result.content?.[0];
93
97
  text.setText(
94
- fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`),
98
+ fillToolBackground(
99
+ `${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
100
+ ),
95
101
  );
96
102
  return text;
97
103
  },
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
- import { ELAPSED_KEY, CHARS_KEY } from "../helpers.js";
8
+ import { CHARS_KEY, ELAPSED_KEY } from "../helpers.js";
9
9
 
10
10
  type ExecuteFn = (
11
11
  tid: string,