@heyhuynhgiabuu/pi-pretty 0.6.26 → 0.6.27

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 (61) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +34 -17
  3. package/dist/config.d.ts +16 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +30 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/fff.d.ts +16 -1
  8. package/dist/fff.d.ts.map +1 -1
  9. package/dist/fff.js +139 -33
  10. package/dist/fff.js.map +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +70 -24
  13. package/dist/index.js.map +1 -1
  14. package/dist/prompt-editor.d.ts +11 -0
  15. package/dist/prompt-editor.d.ts.map +1 -0
  16. package/dist/prompt-editor.js +78 -0
  17. package/dist/prompt-editor.js.map +1 -0
  18. package/dist/render.d.ts +9 -1
  19. package/dist/render.d.ts.map +1 -1
  20. package/dist/render.js +25 -9
  21. package/dist/render.js.map +1 -1
  22. package/dist/tools/bash.d.ts.map +1 -1
  23. package/dist/tools/bash.js +73 -30
  24. package/dist/tools/bash.js.map +1 -1
  25. package/dist/tools/find.d.ts.map +1 -1
  26. package/dist/tools/find.js +29 -10
  27. package/dist/tools/find.js.map +1 -1
  28. package/dist/tools/grep.d.ts.map +1 -1
  29. package/dist/tools/grep.js +20 -10
  30. package/dist/tools/grep.js.map +1 -1
  31. package/dist/tools/ls.d.ts.map +1 -1
  32. package/dist/tools/ls.js +10 -5
  33. package/dist/tools/ls.js.map +1 -1
  34. package/dist/tools/metrics.d.ts +6 -1
  35. package/dist/tools/metrics.d.ts.map +1 -1
  36. package/dist/tools/metrics.js +21 -8
  37. package/dist/tools/metrics.js.map +1 -1
  38. package/dist/tools/read.d.ts +2 -1
  39. package/dist/tools/read.d.ts.map +1 -1
  40. package/dist/tools/read.js +94 -31
  41. package/dist/tools/read.js.map +1 -1
  42. package/dist/types.d.ts +9 -3
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/working-indicator.d.ts +39 -31
  45. package/dist/working-indicator.d.ts.map +1 -1
  46. package/dist/working-indicator.js +114 -87
  47. package/dist/working-indicator.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/config.ts +51 -0
  50. package/src/fff.ts +158 -29
  51. package/src/index.ts +77 -24
  52. package/src/prompt-editor.ts +102 -0
  53. package/src/render.ts +30 -11
  54. package/src/tools/bash.ts +94 -32
  55. package/src/tools/find.ts +39 -17
  56. package/src/tools/grep.ts +26 -15
  57. package/src/tools/ls.ts +19 -10
  58. package/src/tools/metrics.ts +28 -8
  59. package/src/tools/read.ts +123 -31
  60. package/src/types.ts +10 -3
  61. package/src/working-indicator.ts +123 -95
package/src/tools/read.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /* pi-pretty: read tool -- file reading with syntax highlighting and inline image support. */
2
2
 
3
- import { basename, dirname } from "node:path";
3
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
4
  import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
5
+ import { getReadmePath } from "@earendil-works/pi-coding-agent";
5
6
  import {
6
7
  BG_BASE,
7
8
  BG_ERROR,
@@ -14,13 +15,24 @@ import {
14
15
  termWidth,
15
16
  } from "../config.js";
16
17
  import { normalizeLineEndings, shortPath } from "../helpers.js";
17
- import { fillToolBackground, renderFileContent, renderToolError } from "../render.js";
18
+ import { fillToolBackground, fillToolBody, renderFileContent, renderToolError } from "../render.js";
18
19
  import { resolveTextCtor } from "../tui-text.js";
19
20
  import type { ReadDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
20
21
  import { wrapExecuteWithMetrics } from "./metrics.js";
21
22
 
22
23
  type Result = AgentToolResult<Record<string, unknown>>;
23
24
 
25
+ function getPiDocsLabel(filePath: string, cwd: string): string | undefined {
26
+ const packageRoot = dirname(getReadmePath());
27
+ const relativePath = relative(resolve(packageRoot), resolve(cwd, filePath));
28
+ if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
29
+ return undefined;
30
+ }
31
+
32
+ const label = relativePath.split(sep).join("/");
33
+ return label === "README.md" || label.startsWith("docs/") || label.startsWith("examples/") ? label : undefined;
34
+ }
35
+
24
36
  function getSkillName(filePath: string, content: string): string | undefined {
25
37
  if (basename(filePath) !== "SKILL.md") return undefined;
26
38
 
@@ -54,6 +66,7 @@ export function registerReadTool(
54
66
  _fffService: unknown,
55
67
  sdkTool: SdkToolDef,
56
68
  TextComp?: new (t?: string, x?: number, y?: number) => { setText(v: string): void },
69
+ renderContent: typeof renderFileContent = renderFileContent,
57
70
  ): void {
58
71
  const TC = resolveTextCtor(TextComp);
59
72
  const home = process.env.HOME ?? "";
@@ -100,17 +113,18 @@ export function registerReadTool(
100
113
 
101
114
  const path = String(args.path ?? "");
102
115
  const label = theme.fg("error", theme.bold("→ read"));
103
- text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${label} ${theme.fg("toolTitle", path)}`, BG_ERROR));
116
+ text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${label} ${theme.fg("toolTitle", path)}\n`, BG_ERROR));
104
117
  return text;
105
118
  },
106
119
 
107
120
  renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
108
121
  resolveBaseBackground(theme);
109
122
 
123
+ const renderToken = nextReadRenderToken(ctx);
110
124
  const text = ctx.lastComponent ?? new TC("", 0, 0);
111
125
 
112
126
  if (ctx.isError) {
113
- text.setText(fillToolBackground(renderToolError(getText(result) || "Error", theme), BG_ERROR));
127
+ text.setText(renderToolError(getText(result) || "Error", theme));
114
128
  return text;
115
129
  }
116
130
 
@@ -121,7 +135,8 @@ export function registerReadTool(
121
135
  // or unsupported by the terminal.
122
136
  if (d?._type === "readImage") {
123
137
  const note = getText(result);
124
- text.setText(note ? fillToolBackground(note, BG_BASE) : "");
138
+ // Keep fallback text visually separated from the preceding tool row.
139
+ text.setText(note ? fillToolBody(`\n${note}`, BG_BASE) : "");
125
140
  return text;
126
141
  }
127
142
 
@@ -137,12 +152,16 @@ export function registerReadTool(
137
152
  if (!ctx.expanded) {
138
153
  if (skillName) {
139
154
  const header = renderSkillHeader(skillName, false, theme);
140
- text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${header}`, BG_BASE));
155
+ text.setText(fillToolBody(`\n${TOOL_RESULT_INDENT}${header}\n`, BG_BASE));
141
156
  return text;
142
157
  }
158
+ const docsLabel = getPiDocsLabel(filePath, cwd);
159
+ const title = docsLabel
160
+ ? `${theme.fg("toolTitle", theme.bold("→ read docs"))} ${theme.fg("toolTitle", docsLabel)}`
161
+ : `${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}`;
143
162
  text.setText(
144
- fillToolBackground(
145
- `\n${TOOL_RESULT_INDENT}${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}${theme.fg("dim", off2)}\n${TOOL_RESULT_INDENT}${FG_DIM}${total} lines — ctrl+o to expand${RST}\n`,
163
+ fillToolBody(
164
+ `\n${TOOL_RESULT_INDENT}${title}${theme.fg("dim", off2)} ${FG_DIM}${total} lines — ctrl+o to expand${RST}`,
146
165
  BG_BASE,
147
166
  ),
148
167
  );
@@ -150,19 +169,34 @@ export function registerReadTool(
150
169
  }
151
170
  const maxShow = lines.length;
152
171
  const show = lines.slice(0, maxShow);
153
- const nw = Math.max(3, String((d.offset || 0) + total).length);
172
+ const offset = d.offset || 0;
173
+ const nw = Math.max(3, String(offset + total).length);
154
174
  const gw = nw + 3;
155
175
  const cw = Math.max(1, tw - gw);
156
176
 
157
177
  const header = skillName
158
178
  ? renderSkillHeader(skillName, true, theme)
159
179
  : `${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}${theme.fg("dim", off2)}`;
160
- const out: string[] = ["", `${TOOL_RESULT_INDENT}${header}`];
180
+ const highlightRequest: ReadHighlightRequest = {
181
+ content: d.content,
182
+ filePath: d.filePath,
183
+ offset,
184
+ maxLines: maxShow,
185
+ width: cw,
186
+ };
187
+ const cachedHighlight = getCachedReadHighlight(ctx, highlightRequest);
188
+ if (cachedHighlight !== undefined) {
189
+ const highlighted = buildHighlightedRead(cachedHighlight, header, skillName, offset, nw, tw);
190
+ text.setText(fillToolBody(highlighted, BG_BASE));
191
+ return text;
192
+ }
193
+
194
+ const out: string[] = ["", `${TOOL_RESULT_INDENT}${header}`, ""];
161
195
  out.push(`${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(tw - 1)}${RST}`);
162
196
  for (let i = 0; i < show.length; i++) {
163
- const ln = (d.offset || 0) + i + 1;
197
+ const ln = offset + i + 1;
164
198
  const code = show[i] ?? "";
165
- const display = code.length > cw ? code.slice(0, Math.max(0, cw - 1)) + `${FG_DIM}›${RST}` : code;
199
+ const display = code.length > cw ? `${code.slice(0, Math.max(0, cw - 1))}${FG_DIM}›${RST}` : code;
166
200
  const lineNo = String(ln);
167
201
  out.push(
168
202
  `${TOOL_RESULT_INDENT}${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${display}${RST}`,
@@ -171,27 +205,18 @@ export function registerReadTool(
171
205
  if (total > maxShow) {
172
206
  out.push(`${TOOL_RESULT_INDENT}${FG_DIM}… ${total - maxShow} more lines (${total} total)${RST}`);
173
207
  }
174
- out.push("");
175
208
  const rendered = out.join("\n");
176
- text.setText(fillToolBackground(rendered, BG_BASE));
177
- (ctx as any).state._rt = rendered;
209
+ text.setText(fillToolBody(rendered, BG_BASE));
178
210
 
179
- // Async syntax highlighting via Shiki
180
- renderFileContent(d.content, d.filePath, d.offset || 0, maxShow, cw)
211
+ // Async syntax highlighting via Shiki. The component is reused across
212
+ // expansion changes, so stale work must not restore an older view.
213
+ renderContent(d.content, d.filePath, offset, maxShow, cw)
181
214
  .then((hl) => {
182
- const padded = hl
183
- .split("\n")
184
- .map((line, index) => {
185
- const lineNo = String((d.offset || 0) + index + 1);
186
- return `${TOOL_RESULT_INDENT}${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${line}${RST}`;
187
- })
188
- .join("\n");
189
- const divider = skillName
190
- ? `${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(Math.max(1, tw - 1))}${RST}\n`
191
- : "";
192
- const rendered = `\n${TOOL_RESULT_INDENT}${header}\n${divider}${padded}\n`;
193
- text.setText(fillToolBackground(rendered, BG_BASE));
194
- (ctx as any).state._rt = rendered;
215
+ if (!isCurrentReadRender(ctx, renderToken)) return;
216
+ setCachedReadHighlight(ctx, highlightRequest, hl);
217
+ const highlighted = buildHighlightedRead(hl, header, skillName, offset, nw, tw);
218
+ text.setText(fillToolBody(highlighted, BG_BASE));
219
+ ctx.invalidate?.();
195
220
  })
196
221
  .catch(() => {});
197
222
 
@@ -200,7 +225,7 @@ export function registerReadTool(
200
225
 
201
226
  const fc = result.content?.[0];
202
227
  text.setText(
203
- fillToolBackground(
228
+ fillToolBody(
204
229
  `${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
205
230
  BG_BASE,
206
231
  ),
@@ -210,6 +235,73 @@ export function registerReadTool(
210
235
  } as unknown as ToolDefinition<any, any, any>);
211
236
  }
212
237
 
238
+ const READ_RENDER_TOKEN = "__piPrettyReadRenderToken";
239
+ const READ_HIGHLIGHT_CACHE = "__piPrettyReadHighlightCache";
240
+
241
+ type ReadHighlightRequest = {
242
+ content: string;
243
+ filePath: string;
244
+ offset: number;
245
+ maxLines: number;
246
+ width: number;
247
+ };
248
+
249
+ type ReadHighlightCache = ReadHighlightRequest & {
250
+ highlighted: string;
251
+ };
252
+
253
+ function nextReadRenderToken(ctx: RenderCtxLike): number {
254
+ const state = ctx.state;
255
+ const token = typeof state[READ_RENDER_TOKEN] === "number" ? state[READ_RENDER_TOKEN] + 1 : 1;
256
+ state[READ_RENDER_TOKEN] = token;
257
+ return token;
258
+ }
259
+
260
+ function isCurrentReadRender(ctx: RenderCtxLike, token: number): boolean {
261
+ return ctx.state[READ_RENDER_TOKEN] === token;
262
+ }
263
+
264
+ function getCachedReadHighlight(ctx: RenderCtxLike, request: ReadHighlightRequest): string | undefined {
265
+ const cache = ctx.state[READ_HIGHLIGHT_CACHE] as ReadHighlightCache | undefined;
266
+ if (!cache) return undefined;
267
+ if (
268
+ cache.content !== request.content ||
269
+ cache.filePath !== request.filePath ||
270
+ cache.offset !== request.offset ||
271
+ cache.maxLines !== request.maxLines ||
272
+ cache.width !== request.width
273
+ ) {
274
+ return undefined;
275
+ }
276
+ return cache.highlighted;
277
+ }
278
+
279
+ function setCachedReadHighlight(ctx: RenderCtxLike, request: ReadHighlightRequest, highlighted: string): void {
280
+ ctx.state[READ_HIGHLIGHT_CACHE] = {
281
+ ...request,
282
+ highlighted,
283
+ } satisfies ReadHighlightCache;
284
+ }
285
+
286
+ function buildHighlightedRead(
287
+ highlighted: string,
288
+ header: string,
289
+ skillName: string | undefined,
290
+ offset: number,
291
+ nw: number,
292
+ tw: number,
293
+ ): string {
294
+ const padded = highlighted
295
+ .split("\n")
296
+ .map((line, index) => {
297
+ const lineNo = String(offset + index + 1);
298
+ return `${TOOL_RESULT_INDENT}${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${line}${RST}`;
299
+ })
300
+ .join("\n");
301
+ const divider = skillName ? `${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(Math.max(1, tw - 1))}${RST}\n` : "";
302
+ return `\n${TOOL_RESULT_INDENT}${header}\n\n${divider}${padded}`;
303
+ }
304
+
213
305
  function getText(result: Result): string {
214
306
  return (
215
307
  ((result.content ?? []) as TextContent[])
package/src/types.ts CHANGED
@@ -3,7 +3,8 @@
3
3
  */
4
4
 
5
5
  import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
6
- import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
6
+ import type { AgentToolResult, CustomEditor } from "@earendil-works/pi-coding-agent";
7
+ import type { FffInitOptions } from "./fff.js";
7
8
 
8
9
  // ---------------------------------------------------------------------------
9
10
  // Re-export FFF types needed by tools
@@ -29,9 +30,11 @@ export interface ThemeLike {
29
30
 
30
31
  export interface RenderCtxLike {
31
32
  lastComponent?: ComponentLike;
33
+ toolCallId?: string;
32
34
  isError?: boolean;
33
- state: Record<string, string | undefined>;
35
+ state: Record<string, unknown>;
34
36
  expanded?: boolean;
37
+ invalidate?: () => void;
35
38
  }
36
39
 
37
40
  export interface TextLike {
@@ -168,7 +171,7 @@ export interface FffServiceWithCursor extends FffServiceLike {
168
171
 
169
172
  /** FFF lifecycle interface (used by session lifecycle code). */
170
173
  export interface FffService extends FffServiceWithCursor {
171
- ensureFinder(cwd: string): Promise<void>;
174
+ ensureFinder(cwd: string, options?: FffInitOptions): Promise<void>;
172
175
  destroy(): void;
173
176
  isModuleLoaded(): boolean;
174
177
  tryLoadModule(): Promise<boolean>;
@@ -183,6 +186,8 @@ export interface CursorStore {
183
186
  // DI
184
187
  // ---------------------------------------------------------------------------
185
188
 
189
+ export type CustomEditorConstructor = new (...args: ConstructorParameters<typeof CustomEditor>) => CustomEditor;
190
+
186
191
  export interface PiPrettyDeps {
187
192
  sdk?: SdkTools;
188
193
  TextComponent?: new (text?: string, x?: number, y?: number) => ComponentLike;
@@ -191,4 +196,6 @@ export interface PiPrettyDeps {
191
196
  * to the package export. Injectable so tests can verify against the exact
192
197
  * class instance they construct rows from. */
193
198
  assistantMessageComponent?: unknown;
199
+ /** Host CustomEditor class for the live prompt icon; defaults to the package export. */
200
+ customEditorClass?: CustomEditorConstructor;
194
201
  }