@heyhuynhgiabuu/pi-pretty 0.6.26 → 0.6.28

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 +25 -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 +79 -32
  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 +11 -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 +97 -31
  41. package/dist/tools/read.js.map +1 -1
  42. package/dist/types.d.ts +13 -4
  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 +100 -34
  55. package/src/tools/find.ts +39 -17
  56. package/src/tools/grep.ts +26 -15
  57. package/src/tools/ls.ts +20 -10
  58. package/src/tools/metrics.ts +28 -8
  59. package/src/tools/read.ts +126 -31
  60. package/src/types.ts +14 -4
  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 ?? "";
@@ -63,6 +76,9 @@ export function registerReadTool(
63
76
  label: "Read",
64
77
  description: sdkTool.description ?? "Read file contents",
65
78
  parameters: sdkTool.parameters,
79
+ promptSnippet: sdkTool.promptSnippet,
80
+ promptGuidelines: sdkTool.promptGuidelines,
81
+ constrainedSampling: sdkTool.constrainedSampling,
66
82
  renderShell: "self",
67
83
 
68
84
  execute: wrapExecuteWithMetrics(async (tid, params, sig, _upd, ctx: ExtensionContext) => {
@@ -100,17 +116,18 @@ export function registerReadTool(
100
116
 
101
117
  const path = String(args.path ?? "");
102
118
  const label = theme.fg("error", theme.bold("→ read"));
103
- text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${label} ${theme.fg("toolTitle", path)}`, BG_ERROR));
119
+ text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${label} ${theme.fg("toolTitle", path)}\n`, BG_ERROR));
104
120
  return text;
105
121
  },
106
122
 
107
123
  renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
108
124
  resolveBaseBackground(theme);
109
125
 
126
+ const renderToken = nextReadRenderToken(ctx);
110
127
  const text = ctx.lastComponent ?? new TC("", 0, 0);
111
128
 
112
129
  if (ctx.isError) {
113
- text.setText(fillToolBackground(renderToolError(getText(result) || "Error", theme), BG_ERROR));
130
+ text.setText(renderToolError(getText(result) || "Error", theme));
114
131
  return text;
115
132
  }
116
133
 
@@ -121,7 +138,8 @@ export function registerReadTool(
121
138
  // or unsupported by the terminal.
122
139
  if (d?._type === "readImage") {
123
140
  const note = getText(result);
124
- text.setText(note ? fillToolBackground(note, BG_BASE) : "");
141
+ // Keep fallback text visually separated from the preceding tool row.
142
+ text.setText(note ? fillToolBody(`\n${note}`, BG_BASE) : "");
125
143
  return text;
126
144
  }
127
145
 
@@ -137,12 +155,16 @@ export function registerReadTool(
137
155
  if (!ctx.expanded) {
138
156
  if (skillName) {
139
157
  const header = renderSkillHeader(skillName, false, theme);
140
- text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${header}`, BG_BASE));
158
+ text.setText(fillToolBody(`\n${TOOL_RESULT_INDENT}${header}\n`, BG_BASE));
141
159
  return text;
142
160
  }
161
+ const docsLabel = getPiDocsLabel(filePath, cwd);
162
+ const title = docsLabel
163
+ ? `${theme.fg("toolTitle", theme.bold("→ read docs"))} ${theme.fg("toolTitle", docsLabel)}`
164
+ : `${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}`;
143
165
  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`,
166
+ fillToolBody(
167
+ `\n${TOOL_RESULT_INDENT}${title}${theme.fg("dim", off2)} ${FG_DIM}${total} lines — ctrl+o to expand${RST}`,
146
168
  BG_BASE,
147
169
  ),
148
170
  );
@@ -150,19 +172,34 @@ export function registerReadTool(
150
172
  }
151
173
  const maxShow = lines.length;
152
174
  const show = lines.slice(0, maxShow);
153
- const nw = Math.max(3, String((d.offset || 0) + total).length);
175
+ const offset = d.offset || 0;
176
+ const nw = Math.max(3, String(offset + total).length);
154
177
  const gw = nw + 3;
155
178
  const cw = Math.max(1, tw - gw);
156
179
 
157
180
  const header = skillName
158
181
  ? renderSkillHeader(skillName, true, theme)
159
182
  : `${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}${theme.fg("dim", off2)}`;
160
- const out: string[] = ["", `${TOOL_RESULT_INDENT}${header}`];
183
+ const highlightRequest: ReadHighlightRequest = {
184
+ content: d.content,
185
+ filePath: d.filePath,
186
+ offset,
187
+ maxLines: maxShow,
188
+ width: cw,
189
+ };
190
+ const cachedHighlight = getCachedReadHighlight(ctx, highlightRequest);
191
+ if (cachedHighlight !== undefined) {
192
+ const highlighted = buildHighlightedRead(cachedHighlight, header, skillName, offset, nw, tw);
193
+ text.setText(fillToolBody(highlighted, BG_BASE));
194
+ return text;
195
+ }
196
+
197
+ const out: string[] = ["", `${TOOL_RESULT_INDENT}${header}`, ""];
161
198
  out.push(`${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(tw - 1)}${RST}`);
162
199
  for (let i = 0; i < show.length; i++) {
163
- const ln = (d.offset || 0) + i + 1;
200
+ const ln = offset + i + 1;
164
201
  const code = show[i] ?? "";
165
- const display = code.length > cw ? code.slice(0, Math.max(0, cw - 1)) + `${FG_DIM}›${RST}` : code;
202
+ const display = code.length > cw ? `${code.slice(0, Math.max(0, cw - 1))}${FG_DIM}›${RST}` : code;
166
203
  const lineNo = String(ln);
167
204
  out.push(
168
205
  `${TOOL_RESULT_INDENT}${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${display}${RST}`,
@@ -171,27 +208,18 @@ export function registerReadTool(
171
208
  if (total > maxShow) {
172
209
  out.push(`${TOOL_RESULT_INDENT}${FG_DIM}… ${total - maxShow} more lines (${total} total)${RST}`);
173
210
  }
174
- out.push("");
175
211
  const rendered = out.join("\n");
176
- text.setText(fillToolBackground(rendered, BG_BASE));
177
- (ctx as any).state._rt = rendered;
212
+ text.setText(fillToolBody(rendered, BG_BASE));
178
213
 
179
- // Async syntax highlighting via Shiki
180
- renderFileContent(d.content, d.filePath, d.offset || 0, maxShow, cw)
214
+ // Async syntax highlighting via Shiki. The component is reused across
215
+ // expansion changes, so stale work must not restore an older view.
216
+ renderContent(d.content, d.filePath, offset, maxShow, cw)
181
217
  .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;
218
+ if (!isCurrentReadRender(ctx, renderToken)) return;
219
+ setCachedReadHighlight(ctx, highlightRequest, hl);
220
+ const highlighted = buildHighlightedRead(hl, header, skillName, offset, nw, tw);
221
+ text.setText(fillToolBody(highlighted, BG_BASE));
222
+ ctx.invalidate?.();
195
223
  })
196
224
  .catch(() => {});
197
225
 
@@ -200,7 +228,7 @@ export function registerReadTool(
200
228
 
201
229
  const fc = result.content?.[0];
202
230
  text.setText(
203
- fillToolBackground(
231
+ fillToolBody(
204
232
  `${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
205
233
  BG_BASE,
206
234
  ),
@@ -210,6 +238,73 @@ export function registerReadTool(
210
238
  } as unknown as ToolDefinition<any, any, any>);
211
239
  }
212
240
 
241
+ const READ_RENDER_TOKEN = "__piPrettyReadRenderToken";
242
+ const READ_HIGHLIGHT_CACHE = "__piPrettyReadHighlightCache";
243
+
244
+ type ReadHighlightRequest = {
245
+ content: string;
246
+ filePath: string;
247
+ offset: number;
248
+ maxLines: number;
249
+ width: number;
250
+ };
251
+
252
+ type ReadHighlightCache = ReadHighlightRequest & {
253
+ highlighted: string;
254
+ };
255
+
256
+ function nextReadRenderToken(ctx: RenderCtxLike): number {
257
+ const state = ctx.state;
258
+ const token = typeof state[READ_RENDER_TOKEN] === "number" ? state[READ_RENDER_TOKEN] + 1 : 1;
259
+ state[READ_RENDER_TOKEN] = token;
260
+ return token;
261
+ }
262
+
263
+ function isCurrentReadRender(ctx: RenderCtxLike, token: number): boolean {
264
+ return ctx.state[READ_RENDER_TOKEN] === token;
265
+ }
266
+
267
+ function getCachedReadHighlight(ctx: RenderCtxLike, request: ReadHighlightRequest): string | undefined {
268
+ const cache = ctx.state[READ_HIGHLIGHT_CACHE] as ReadHighlightCache | undefined;
269
+ if (!cache) return undefined;
270
+ if (
271
+ cache.content !== request.content ||
272
+ cache.filePath !== request.filePath ||
273
+ cache.offset !== request.offset ||
274
+ cache.maxLines !== request.maxLines ||
275
+ cache.width !== request.width
276
+ ) {
277
+ return undefined;
278
+ }
279
+ return cache.highlighted;
280
+ }
281
+
282
+ function setCachedReadHighlight(ctx: RenderCtxLike, request: ReadHighlightRequest, highlighted: string): void {
283
+ ctx.state[READ_HIGHLIGHT_CACHE] = {
284
+ ...request,
285
+ highlighted,
286
+ } satisfies ReadHighlightCache;
287
+ }
288
+
289
+ function buildHighlightedRead(
290
+ highlighted: string,
291
+ header: string,
292
+ skillName: string | undefined,
293
+ offset: number,
294
+ nw: number,
295
+ tw: number,
296
+ ): string {
297
+ const padded = highlighted
298
+ .split("\n")
299
+ .map((line, index) => {
300
+ const lineNo = String(offset + index + 1);
301
+ return `${TOOL_RESULT_INDENT}${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${line}${RST}`;
302
+ })
303
+ .join("\n");
304
+ const divider = skillName ? `${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(Math.max(1, tw - 1))}${RST}\n` : "";
305
+ return `\n${TOOL_RESULT_INDENT}${header}\n\n${divider}${padded}`;
306
+ }
307
+
213
308
  function getText(result: Result): string {
214
309
  return (
215
310
  ((result.content ?? []) as TextContent[])
package/src/types.ts CHANGED
@@ -2,8 +2,9 @@
2
2
  * pi-pretty shared types.
3
3
  */
4
4
 
5
- import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
6
- import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
5
+ import type { ConstrainedSamplingConfig, ImageContent, TextContent } from "@earendil-works/pi-ai";
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 {
@@ -133,6 +136,9 @@ export interface SdkToolDef {
133
136
  description?: string;
134
137
  label?: string;
135
138
  parameters?: unknown;
139
+ promptSnippet?: string;
140
+ promptGuidelines?: string[];
141
+ constrainedSampling?: false | ConstrainedSamplingConfig;
136
142
  execute: (...args: any[]) => Promise<AgentToolResult<any>>;
137
143
  }
138
144
 
@@ -168,7 +174,7 @@ export interface FffServiceWithCursor extends FffServiceLike {
168
174
 
169
175
  /** FFF lifecycle interface (used by session lifecycle code). */
170
176
  export interface FffService extends FffServiceWithCursor {
171
- ensureFinder(cwd: string): Promise<void>;
177
+ ensureFinder(cwd: string, options?: FffInitOptions): Promise<void>;
172
178
  destroy(): void;
173
179
  isModuleLoaded(): boolean;
174
180
  tryLoadModule(): Promise<boolean>;
@@ -183,6 +189,8 @@ export interface CursorStore {
183
189
  // DI
184
190
  // ---------------------------------------------------------------------------
185
191
 
192
+ export type CustomEditorConstructor = new (...args: ConstructorParameters<typeof CustomEditor>) => CustomEditor;
193
+
186
194
  export interface PiPrettyDeps {
187
195
  sdk?: SdkTools;
188
196
  TextComponent?: new (text?: string, x?: number, y?: number) => ComponentLike;
@@ -191,4 +199,6 @@ export interface PiPrettyDeps {
191
199
  * to the package export. Injectable so tests can verify against the exact
192
200
  * class instance they construct rows from. */
193
201
  assistantMessageComponent?: unknown;
202
+ /** Host CustomEditor class for the live prompt icon; defaults to the package export. */
203
+ customEditorClass?: CustomEditorConstructor;
194
204
  }