@heyhuynhgiabuu/pi-pretty 0.6.30 → 0.7.0

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 (55) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +21 -0
  3. package/dist/claude-style.d.ts +45 -0
  4. package/dist/claude-style.d.ts.map +1 -0
  5. package/dist/claude-style.js +582 -0
  6. package/dist/claude-style.js.map +1 -0
  7. package/dist/config.d.ts +2 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +2 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +22 -12
  13. package/dist/index.js.map +1 -1
  14. package/dist/prompt-editor.d.ts +4 -1
  15. package/dist/prompt-editor.d.ts.map +1 -1
  16. package/dist/prompt-editor.js +27 -12
  17. package/dist/prompt-editor.js.map +1 -1
  18. package/dist/render.d.ts +10 -7
  19. package/dist/render.d.ts.map +1 -1
  20. package/dist/render.js +41 -25
  21. package/dist/render.js.map +1 -1
  22. package/dist/tools/bash.d.ts.map +1 -1
  23. package/dist/tools/bash.js +17 -13
  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 +15 -17
  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 +10 -8
  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 -9
  33. package/dist/tools/ls.js.map +1 -1
  34. package/dist/tools/read.d.ts.map +1 -1
  35. package/dist/tools/read.js +25 -21
  36. package/dist/tools/read.js.map +1 -1
  37. package/dist/types.d.ts +9 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/working-indicator.d.ts +2 -0
  40. package/dist/working-indicator.d.ts.map +1 -1
  41. package/dist/working-indicator.js +20 -8
  42. package/dist/working-indicator.js.map +1 -1
  43. package/package.json +5 -5
  44. package/src/claude-style.ts +650 -0
  45. package/src/config.ts +3 -0
  46. package/src/index.ts +35 -15
  47. package/src/prompt-editor.ts +26 -11
  48. package/src/render.ts +54 -24
  49. package/src/tools/bash.ts +28 -23
  50. package/src/tools/find.ts +20 -16
  51. package/src/tools/grep.ts +10 -6
  52. package/src/tools/ls.ts +17 -8
  53. package/src/tools/read.ts +29 -30
  54. package/src/types.ts +9 -0
  55. package/src/working-indicator.ts +20 -8
package/src/tools/ls.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /* pi-pretty: ls tool -- directory listing with styled output. */
2
2
 
3
3
  import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
4
- import { BG_ERROR, FG_DIM, RST, resolveBaseBackground, TOOL_RESULT_INDENT } from "../config.js";
4
+ import { BG_ERROR, FG_DIM, RST, resolveBaseBackground } from "../config.js";
5
5
  import { shortPath } from "../helpers.js";
6
6
  import {
7
7
  fillToolBackground,
@@ -11,6 +11,7 @@ import {
11
11
  renderToolMetrics,
12
12
  renderTree,
13
13
  setCollapsedToolTitle,
14
+ toolIndent,
14
15
  } from "../render.js";
15
16
  import { resolveTextCtor } from "../tui-text.js";
16
17
  import type { LsDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
@@ -60,8 +61,9 @@ export function registerLsTool(
60
61
  let out = theme.fg("toolTitle", theme.bold("ls"));
61
62
  if (path) out += ` ${theme.fg("accent", path)}`;
62
63
  if (limit !== undefined && limit !== null) out += theme.fg("toolOutput", ` (limit ${limit})`);
64
+ const ind = toolIndent(ctx);
63
65
  const renderTitle = (suffix = ""): string =>
64
- fillToolBackground(`\n${TOOL_RESULT_INDENT}${out}${suffix}\n`, ctx.isError ? BG_ERROR : undefined);
66
+ fillToolBackground(`\n${ind}${out}${suffix}\n`, ctx.isError ? BG_ERROR : undefined, undefined, ind);
65
67
  rememberToolTitle(ctx, text, renderTitle);
66
68
  text.setText(renderTitle());
67
69
  return text;
@@ -71,25 +73,29 @@ export function registerLsTool(
71
73
  resolveBaseBackground(theme);
72
74
 
73
75
  const text = ctx.lastComponent ?? new TC("", 0, 0);
76
+ const ind = toolIndent(ctx);
74
77
  if (ctx.isError) {
75
- text.setText(renderToolError(getText(result) || "Error", theme));
78
+ text.setText(renderToolError(getText(result) || "Error", theme, ind));
76
79
  return text;
77
80
  }
78
81
  const d = result.details as LsDetails | undefined;
79
82
  if (d?._type === "lsResult" && d.text) {
80
83
  if (!ctx.expanded) {
81
- const summary = `${FG_DIM}${d.entryCount} entries — ctrl+o to expand${RST}${renderToolMetrics(result)}`;
84
+ const summary = `${FG_DIM}${d.entryCount} entries — ctrl+o to expand${RST}${renderToolMetrics(result, ctx)}`;
82
85
  if (setCollapsedToolTitle(ctx, text, ` ${summary}`)) return text;
83
- text.setText(fillToolBody(`${TOOL_RESULT_INDENT}${summary}`));
86
+ text.setText(fillToolBody(`${ind}${summary}`, undefined, undefined, ind));
84
87
  return text;
85
88
  }
86
89
  const rendered = renderTree(d.text, d.path)
87
90
  .split("\n")
88
- .map((l) => `${TOOL_RESULT_INDENT}${l}`)
91
+ .map((l) => `${ind}${l}`)
89
92
  .join("\n");
90
93
  text.setText(
91
94
  fillToolBody(
92
- `${TOOL_RESULT_INDENT}${FG_DIM}${d.entryCount} entries${RST}${renderToolMetrics(result)}\n${rendered}`,
95
+ `${ind}${FG_DIM}${d.entryCount} entries${RST}${renderToolMetrics(result, ctx)}\n${rendered}`,
96
+ undefined,
97
+ undefined,
98
+ ind,
93
99
  ),
94
100
  );
95
101
  return text;
@@ -97,7 +103,10 @@ export function registerLsTool(
97
103
  const fc = result.content?.[0];
98
104
  text.setText(
99
105
  fillToolBody(
100
- `${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
106
+ `${ind}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
107
+ undefined,
108
+ undefined,
109
+ ind,
101
110
  ),
102
111
  );
103
112
  return text;
package/src/tools/read.ts CHANGED
@@ -3,19 +3,9 @@
3
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
5
  import { getReadmePath } from "@earendil-works/pi-coding-agent";
6
- import {
7
- BG_BASE,
8
- BG_ERROR,
9
- FG_DIM,
10
- FG_LNUM,
11
- FG_RULE,
12
- RST,
13
- resolveBaseBackground,
14
- TOOL_RESULT_INDENT,
15
- termWidth,
16
- } from "../config.js";
6
+ import { BG_BASE, BG_ERROR, FG_DIM, FG_LNUM, FG_RULE, RST, resolveBaseBackground, termWidth } from "../config.js";
17
7
  import { normalizeLineEndings, shortPath } from "../helpers.js";
18
- import { fillToolBackground, fillToolBody, renderFileContent, renderToolError } from "../render.js";
8
+ import { fillToolBackground, fillToolBody, renderFileContent, renderToolError, toolIndent } from "../render.js";
19
9
  import { resolveTextCtor } from "../tui-text.js";
20
10
  import type { ReadDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
21
11
  import { wrapExecuteWithMetrics } from "./metrics.js";
@@ -79,6 +69,7 @@ export function registerReadTool(
79
69
  promptSnippet: sdkTool.promptSnippet,
80
70
  promptGuidelines: sdkTool.promptGuidelines,
81
71
  constrainedSampling: sdkTool.constrainedSampling,
72
+ outputSchema: sdkTool.outputSchema as ToolDefinition["outputSchema"],
82
73
  renderShell: "self",
83
74
 
84
75
  execute: wrapExecuteWithMetrics(async (tid, params, sig, _upd, ctx: ExtensionContext) => {
@@ -114,9 +105,10 @@ export function registerReadTool(
114
105
  return text;
115
106
  }
116
107
 
108
+ const ind = toolIndent(ctx);
117
109
  const path = String(args.path ?? "");
118
110
  const label = theme.fg("error", theme.bold("→ read"));
119
- text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${label} ${theme.fg("toolTitle", path)}\n`, BG_ERROR));
111
+ text.setText(fillToolBackground(`\n${ind}${label} ${theme.fg("toolTitle", path)}\n`, BG_ERROR, undefined, ind));
120
112
  return text;
121
113
  },
122
114
 
@@ -125,9 +117,10 @@ export function registerReadTool(
125
117
 
126
118
  const renderToken = nextReadRenderToken(ctx);
127
119
  const text = ctx.lastComponent ?? new TC("", 0, 0);
120
+ const ind = toolIndent(ctx);
128
121
 
129
122
  if (ctx.isError) {
130
- text.setText(renderToolError(getText(result) || "Error", theme));
123
+ text.setText(renderToolError(getText(result) || "Error", theme, ind));
131
124
  return text;
132
125
  }
133
126
 
@@ -155,7 +148,7 @@ export function registerReadTool(
155
148
  if (!ctx.expanded) {
156
149
  if (skillName) {
157
150
  const header = renderSkillHeader(skillName, false, theme);
158
- text.setText(fillToolBody(`\n${TOOL_RESULT_INDENT}${header}\n`, BG_BASE));
151
+ text.setText(fillToolBody(`\n${ind}${header}\n`, BG_BASE, undefined, ind));
159
152
  return text;
160
153
  }
161
154
  const docsLabel = getPiDocsLabel(filePath, cwd);
@@ -164,8 +157,10 @@ export function registerReadTool(
164
157
  : `${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}`;
165
158
  text.setText(
166
159
  fillToolBody(
167
- `\n${TOOL_RESULT_INDENT}${title}${theme.fg("dim", off2)} ${FG_DIM}${total} lines — ctrl+o to expand${RST}`,
160
+ `\n${ind}${title}${theme.fg("dim", off2)} ${FG_DIM}${total} lines — ctrl+o to expand${RST}`,
168
161
  BG_BASE,
162
+ undefined,
163
+ ind,
169
164
  ),
170
165
  );
171
166
  return text;
@@ -175,7 +170,8 @@ export function registerReadTool(
175
170
  const offset = d.offset || 0;
176
171
  const nw = Math.max(3, String(offset + total).length);
177
172
  const gw = nw + 3;
178
- const cw = Math.max(1, tw - gw);
173
+ // Default pad 1 keeps its historical budget; extra padding is subtracted so rows do not grow.
174
+ const cw = Math.max(1, tw - gw - Math.max(0, ind.length - 1));
179
175
 
180
176
  const header = skillName
181
177
  ? renderSkillHeader(skillName, true, theme)
@@ -189,27 +185,27 @@ export function registerReadTool(
189
185
  };
190
186
  const cachedHighlight = getCachedReadHighlight(ctx, highlightRequest);
191
187
  if (cachedHighlight !== undefined) {
192
- const highlighted = buildHighlightedRead(cachedHighlight, header, skillName, offset, nw, tw);
193
- text.setText(fillToolBody(highlighted, BG_BASE));
188
+ const highlighted = buildHighlightedRead(cachedHighlight, header, skillName, offset, nw, tw, ind);
189
+ text.setText(fillToolBody(highlighted, BG_BASE, undefined, ind));
194
190
  return text;
195
191
  }
196
192
 
197
- const out: string[] = ["", `${TOOL_RESULT_INDENT}${header}`, ""];
198
- out.push(`${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(tw - 1)}${RST}`);
193
+ const out: string[] = ["", `${ind}${header}`, ""];
194
+ out.push(`${ind}${FG_RULE}${"─".repeat(Math.max(1, tw - ind.length))}${RST}`);
199
195
  for (let i = 0; i < show.length; i++) {
200
196
  const ln = offset + i + 1;
201
197
  const code = show[i] ?? "";
202
198
  const display = code.length > cw ? `${code.slice(0, Math.max(0, cw - 1))}${FG_DIM}›${RST}` : code;
203
199
  const lineNo = String(ln);
204
200
  out.push(
205
- `${TOOL_RESULT_INDENT}${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${display}${RST}`,
201
+ `${ind}${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${display}${RST}`,
206
202
  );
207
203
  }
208
204
  if (total > maxShow) {
209
- out.push(`${TOOL_RESULT_INDENT}${FG_DIM}… ${total - maxShow} more lines (${total} total)${RST}`);
205
+ out.push(`${ind}${FG_DIM}… ${total - maxShow} more lines (${total} total)${RST}`);
210
206
  }
211
207
  const rendered = out.join("\n");
212
- text.setText(fillToolBody(rendered, BG_BASE));
208
+ text.setText(fillToolBody(rendered, BG_BASE, undefined, ind));
213
209
 
214
210
  // Async syntax highlighting via Shiki. The component is reused across
215
211
  // expansion changes, so stale work must not restore an older view.
@@ -217,8 +213,8 @@ export function registerReadTool(
217
213
  .then((hl) => {
218
214
  if (!isCurrentReadRender(ctx, renderToken)) return;
219
215
  setCachedReadHighlight(ctx, highlightRequest, hl);
220
- const highlighted = buildHighlightedRead(hl, header, skillName, offset, nw, tw);
221
- text.setText(fillToolBody(highlighted, BG_BASE));
216
+ const highlighted = buildHighlightedRead(hl, header, skillName, offset, nw, tw, ind);
217
+ text.setText(fillToolBody(highlighted, BG_BASE, undefined, ind));
222
218
  ctx.invalidate?.();
223
219
  })
224
220
  .catch(() => {});
@@ -229,8 +225,10 @@ export function registerReadTool(
229
225
  const fc = result.content?.[0];
230
226
  text.setText(
231
227
  fillToolBody(
232
- `${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
228
+ `${ind}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
233
229
  BG_BASE,
230
+ undefined,
231
+ ind,
234
232
  ),
235
233
  );
236
234
  return text;
@@ -293,16 +291,17 @@ function buildHighlightedRead(
293
291
  offset: number,
294
292
  nw: number,
295
293
  tw: number,
294
+ ind: string,
296
295
  ): string {
297
296
  const padded = highlighted
298
297
  .split("\n")
299
298
  .map((line, index) => {
300
299
  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}`;
300
+ return `${ind}${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${line}${RST}`;
302
301
  })
303
302
  .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}`;
303
+ const divider = skillName ? `${ind}${FG_RULE}${"─".repeat(Math.max(1, tw - ind.length))}${RST}\n` : "";
304
+ return `\n${ind}${header}\n\n${divider}${padded}`;
306
305
  }
307
306
 
308
307
  function getText(result: Result): string {
package/src/types.ts CHANGED
@@ -34,7 +34,16 @@ export interface RenderCtxLike {
34
34
  isError?: boolean;
35
35
  state: Record<string, unknown>;
36
36
  expanded?: boolean;
37
+ /** Tool call arguments as streamed so far. */
38
+ args?: Record<string, unknown>;
39
+ isPartial?: boolean;
40
+ /** False while the tool call is still streaming its arguments. */
41
+ executionStarted?: boolean;
37
42
  invalidate?: () => void;
43
+ /** Host horizontal padding (pi >= 1.1). Absent on older hosts; renderers fall back to 1. */
44
+ outputPad?: number;
45
+ /** Final host-measured execution time in ms; undefined while running or on older hosts. */
46
+ durationMs?: number;
38
47
  }
39
48
 
40
49
  export interface TextLike {
@@ -14,6 +14,7 @@
14
14
  * phase. The extension adds no interrupt hint or token-count suffix.
15
15
  */
16
16
 
17
+ import { keyText } from "@earendil-works/pi-coding-agent";
17
18
  import { truncateToWidth } from "@earendil-works/pi-tui";
18
19
  import { FG_BLUE, FG_DIM, FG_MUTED, type ThinkingIndicatorConfig, type WorkingIndicatorConfig } from "./config.js";
19
20
  import { dimAccentHex, hexToAnsiFg, sessionAccentHex } from "./session-color.js";
@@ -539,7 +540,7 @@ export function installPerRowThinkingLabels(componentClass: unknown): PerRowThin
539
540
  if (typeof ts !== "number") return incoming;
540
541
  if (ts === state.activeTs) return incoming;
541
542
  const ms = state.completed.get(ts);
542
- return ms === undefined ? THINKING_LABEL : `${THOUGHT_LABEL} ${formatThinkingDuration(ms)}`;
543
+ return ms === undefined ? thinkingLabel() : thoughtLabel(ms);
543
544
  } catch {
544
545
  return incoming;
545
546
  }
@@ -588,9 +589,20 @@ export interface ThinkingTimer {
588
589
  restore(): void;
589
590
  }
590
591
 
591
- const THINKING_LABEL = " Thinking...";
592
- const THOUGHT_LABEL = " Thought for";
593
- const padThinkingLabel = (label: string): string => (label.startsWith(" ") ? label : ` ${label}`);
592
+ let claudeThinkingLabels = false;
593
+ /** claudecode tool style: flush-left `thinking` / `thought 3s` instead of ` Thinking...` / ` Thought for 3s`. */
594
+ export function setClaudeThinkingLabels(enabled: boolean): void {
595
+ claudeThinkingLabels = enabled;
596
+ }
597
+ const thinkingLabel = (): string => (claudeThinkingLabels ? "thinking" : " Thinking...");
598
+ const padThinkingLabel = (label: string): string =>
599
+ claudeThinkingLabels || label.startsWith(" ") ? label : ` ${label}`;
600
+ /** Settled label for a finished thinking run; claudecode style appends the toggle hint when bound. */
601
+ const thoughtLabel = (ms: number): string => {
602
+ if (!claudeThinkingLabels) return ` Thought for ${formatThinkingDuration(ms)}`;
603
+ const key = keyText("app.thinking.toggle");
604
+ return `thought ${formatThinkingDuration(ms)}${key ? ` (${key} to expand)` : ""}`;
605
+ };
594
606
 
595
607
  /** Format sub-second elapsed time as milliseconds, then use compact whole seconds. */
596
608
  export function formatThinkingDuration(elapsedMs: number): string {
@@ -626,13 +638,13 @@ export function createThinkingTimer(
626
638
  return {
627
639
  tick(): void {
628
640
  if (terminal) return;
629
- animator.tick(`${THINKING_LABEL} ${duration()}`);
641
+ animator.tick(`${thinkingLabel()} ${duration()}`);
630
642
  },
631
643
  complete(): number | undefined {
632
644
  if (terminal) return undefined;
633
645
  terminal = true;
634
646
  const ms = elapsed();
635
- animator.show(`${THOUGHT_LABEL} ${formatThinkingDuration(ms)}`);
647
+ animator.show(thoughtLabel(ms));
636
648
  return ms;
637
649
  },
638
650
  restore(): void {
@@ -702,7 +714,7 @@ export function createThinkingLabelAnimator(
702
714
  }).frames.map((frame) => `${leftPadding}${frame}`);
703
715
  };
704
716
 
705
- let currentLabel = THINKING_LABEL;
717
+ let currentLabel = thinkingLabel();
706
718
  let frames = buildLabelFrames(currentLabel);
707
719
 
708
720
  let index = 0;
@@ -711,7 +723,7 @@ export function createThinkingLabelAnimator(
711
723
  get frames(): readonly string[] {
712
724
  return frames;
713
725
  },
714
- tick(label = THINKING_LABEL): void {
726
+ tick(label = thinkingLabel()): void {
715
727
  const paddedLabel = padThinkingLabel(label);
716
728
  if (paddedLabel !== currentLabel) {
717
729
  frames = buildLabelFrames(paddedLabel);