@pify/pretty 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -22,10 +22,18 @@ Expand with pi's standard toggle (Ctrl+O on a tool block). Failures always show
22
22
  Each renderer toggles independently (persisted per session):
23
23
 
24
24
  ```
25
- /pretty # show which renderers are on
26
- /pretty bash # toggle one back to pi's default rendering
25
+ /pretty # show which renderers are on
26
+ /pretty bash # toggle one back to pi's default rendering
27
+ /pretty bash grep # toggle several at once (v0.2)
28
+ /pretty off # everything back to pi's rendering (v0.2)
29
+ /pretty on read # explicit on/off instead of toggling (v0.2)
30
+ /pretty reset # all renderers back on (v0.2)
27
31
  ```
28
32
 
33
+ Aliases are accepted where they're obvious: `list`/`dir` → `ls`, `search`/`rg` → `grep`, `cat` → `read`, `sh`/`shell` → `bash`.
34
+
35
+ Expanded bodies are capped at 200 lines (v0.2) — a 5,000-line diff or grep result used to render in full and scroll the conversation away.
36
+
29
37
  ## How it works
30
38
 
31
39
  The official `built-in-tool-renderer` pattern: each tool is re-registered with `createReadTool()`/`createBashTool()`/… delegating `execute` untouched, overriding only `renderCall`/`renderResult`. Highlighting uses pi's exported `highlightCode` + `getLanguageFromPath` — zero extra dependencies, colors always match your theme.
@@ -28,8 +28,16 @@ import {
28
28
  } from "@earendil-works/pi-coding-agent";
29
29
  import { Text } from "@earendil-works/pi-tui";
30
30
 
31
- import { PRETTY_CONFIG, isPrettyTool, replayBranch, statusLines, toggleTool } from "../src/config.ts";
31
+ import {
32
+ PRETTY_CONFIG,
33
+ PRETTY_USAGE,
34
+ applyCommand,
35
+ parsePrettyCommand,
36
+ replayBranch,
37
+ statusLines,
38
+ } from "../src/config.ts";
32
39
  import { colorizeDiff, diffStats, statsLabel } from "../src/diff.ts";
40
+ import { preview } from "../src/preview.ts";
33
41
  import {
34
42
  bashCall,
35
43
  bashSummary,
@@ -50,8 +58,6 @@ import {
50
58
  type ThemeLike,
51
59
  } from "../src/types.ts";
52
60
 
53
- const PREVIEW_LINES = 12;
54
-
55
61
  type AnyTool = {
56
62
  name: string;
57
63
  description: string;
@@ -68,13 +74,6 @@ export default function pretty(pi: ExtensionAPI) {
68
74
  return isRecord(result) && result.isError === true;
69
75
  }
70
76
 
71
- function clip(text: string, expanded: boolean): string {
72
- if (expanded) return text;
73
- const lines = text.split("\n");
74
- if (lines.length <= PREVIEW_LINES) return text;
75
- return `${lines.slice(0, PREVIEW_LINES).join("\n")}\n…`;
76
- }
77
-
78
77
  function buildOriginals(cwd: string): Record<PrettyTool, AnyTool> {
79
78
  return {
80
79
  read: createReadTool(cwd) as unknown as AnyTool,
@@ -133,11 +132,11 @@ export default function pretty(pi: ExtensionAPI) {
133
132
  ) => {
134
133
  const output = textContent(result);
135
134
  if (options.isPartial) {
136
- return new Text(`${theme.fg("warning", "Running…")}\n${clip(output, false)}`, 0, 0);
135
+ return new Text(`${theme.fg("warning", "Running…")}\n${preview(output, false)}`, 0, 0);
137
136
  }
138
137
  const failed = isFailed(result);
139
138
  const summary = bashSummary(theme, output, failed);
140
- const body = output && (options.expanded || failed) ? `\n${clip(output, options.expanded === true)}` : "";
139
+ const body = output && (options.expanded || failed) ? `\n${preview(output, options.expanded === true)}` : "";
141
140
  return new Text(summary + body, 0, 0);
142
141
  },
143
142
  };
@@ -159,7 +158,7 @@ export default function pretty(pi: ExtensionAPI) {
159
158
  : "";
160
159
  const stats = statsLabel(theme, diffStats(diff));
161
160
  if (!options.expanded) return new Text(stats, 0, 0);
162
- return new Text(`${stats}\n${colorizeDiff(theme, diff)}`, 0, 0);
161
+ return new Text(`${stats}\n${colorizeDiff(theme, preview(diff, true))}`, 0, 0);
163
162
  },
164
163
  };
165
164
  case "write":
@@ -200,7 +199,7 @@ export default function pretty(pi: ExtensionAPI) {
200
199
  tool === "grep" ? { one: "match", many: "matches" } : { one: "result", many: "results" },
201
200
  );
202
201
  if (!options.expanded || failed || !output) return new Text(summary, 0, 0);
203
- return new Text(`${summary}\n${clip(output, true)}`, 0, 0);
202
+ return new Text(`${summary}\n${preview(output, true)}`, 0, 0);
204
203
  },
205
204
  };
206
205
  case "ls":
@@ -216,7 +215,7 @@ export default function pretty(pi: ExtensionAPI) {
216
215
  const failed = isFailed(result);
217
216
  const summary = matchSummary(theme, output, failed, { one: "entry", many: "entries" });
218
217
  if (!options.expanded || failed) return new Text(summary, 0, 0);
219
- return new Text(`${summary}\n${clip(output, true)}`, 0, 0);
218
+ return new Text(`${summary}\n${preview(output, true)}`, 0, 0);
220
219
  },
221
220
  };
222
221
  }
@@ -254,23 +253,31 @@ export default function pretty(pi: ExtensionAPI) {
254
253
  // ── Command ──────────────────────────────────────────────────────────
255
254
 
256
255
  pi.registerCommand("pretty", {
257
- description: "Toggle pretty tool rendering: /pretty [read|bash|edit|write|grep|find|ls]",
256
+ description: "Pretty tool rendering: /pretty [status | on|off [tool…] | reset | <tool…>]",
258
257
  handler: async (args, ctx: ExtensionContext) => {
259
- if (!ctx.hasUI) return;
260
- const target = (args ?? "").trim().toLowerCase();
261
- if (!target) {
262
- ctx.ui.notify(`Pretty renderers\n${statusLines(config).join("\n")}\nToggle with /pretty <tool>`, "info");
258
+ const command = parsePrettyCommand(args ?? "");
259
+ if (command.kind === "error") {
260
+ if (ctx.hasUI) ctx.ui.notify(command.message, "warning");
263
261
  return;
264
262
  }
265
- if (!isPrettyTool(target)) {
266
- ctx.ui.notify(`Unknown tool "${target}". Tools: read, bash, edit, write, grep, find, ls`, "warning");
263
+ if (command.kind === "status") {
264
+ if (ctx.hasUI) {
265
+ ctx.ui.notify(`Pretty renderers\n${statusLines(config).join("\n")}\n${PRETTY_USAGE}`, "info");
266
+ }
267
267
  return;
268
268
  }
269
- config = toggleTool(config, target);
269
+
270
+ const { config: next, changed } = applyCommand(config, command);
271
+ config = next;
270
272
  pi.appendEntry(PRETTY_CONFIG, config);
271
- applyTool(target);
272
- const on = !config.disabled.includes(target);
273
- ctx.ui.notify(`pretty ${target}: ${on ? "on" : "off"}`, "info");
273
+ for (const tool of changed) applyTool(tool);
274
+ if (!ctx.hasUI) return;
275
+ ctx.ui.notify(
276
+ changed.length === 0
277
+ ? "Nothing changed."
278
+ : changed.map((t) => `pretty ${t}: ${config.disabled.includes(t) ? "off" : "on"}`).join("\n"),
279
+ "info",
280
+ );
274
281
  },
275
282
  });
276
283
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pify/pretty",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Compact, theme-aware rendering for pi's built-in tools: one-line summaries, syntax-highlighted reads, colorized diffs — behavior untouched",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -27,12 +27,13 @@
27
27
  "files": [
28
28
  "extensions",
29
29
  "src",
30
- "skills",
31
30
  "README.md",
32
31
  "LICENSE"
33
32
  ],
34
33
  "pi": {
35
- "extensions": ["./extensions/pretty.ts"]
34
+ "extensions": [
35
+ "./extensions/pretty.ts"
36
+ ]
36
37
  },
37
38
  "scripts": {
38
39
  "typecheck": "tsc --noEmit",
@@ -44,8 +45,12 @@
44
45
  "@earendil-works/pi-tui": "*"
45
46
  },
46
47
  "peerDependenciesMeta": {
47
- "@earendil-works/pi-coding-agent": { "optional": true },
48
- "@earendil-works/pi-tui": { "optional": true }
48
+ "@earendil-works/pi-coding-agent": {
49
+ "optional": true
50
+ },
51
+ "@earendil-works/pi-tui": {
52
+ "optional": true
53
+ }
49
54
  },
50
55
  "devDependencies": {
51
56
  "@earendil-works/pi-coding-agent": "^0.84.4",
package/src/config.ts CHANGED
@@ -33,6 +33,88 @@ export function toggleTool(config: PrettyConfig, tool: PrettyTool): PrettyConfig
33
33
  : { disabled: [...config.disabled, tool] };
34
34
  }
35
35
 
36
+ export const PRETTY_USAGE = "Usage: /pretty [status | on|off [tool…] | reset | <tool…>] tools: read, bash, edit, write, grep, find, ls";
37
+
38
+ /** Aliases the model or a hurried user is likely to type. */
39
+ const ALIASES: Record<string, PrettyTool> = {
40
+ list: "ls",
41
+ dir: "ls",
42
+ search: "grep",
43
+ rg: "grep",
44
+ cat: "read",
45
+ shell: "bash",
46
+ sh: "bash",
47
+ };
48
+
49
+ export type PrettyCommand =
50
+ | { kind: "status" }
51
+ | { kind: "toggle"; tools: PrettyTool[] }
52
+ | { kind: "set"; tools: PrettyTool[]; on: boolean }
53
+ | { kind: "reset" }
54
+ | { kind: "error"; message: string };
55
+
56
+ function resolveTools(words: string[]): { tools: PrettyTool[]; unknown: string[] } {
57
+ const tools: PrettyTool[] = [];
58
+ const unknown: string[] = [];
59
+ for (const word of words) {
60
+ if (word === "all") {
61
+ tools.push(...PRETTY_TOOLS);
62
+ continue;
63
+ }
64
+ const name = ALIASES[word] ?? word;
65
+ if (isPrettyTool(name)) {
66
+ if (!tools.includes(name)) tools.push(name);
67
+ } else {
68
+ unknown.push(word);
69
+ }
70
+ }
71
+ return { tools, unknown };
72
+ }
73
+
74
+ export function parsePrettyCommand(raw: string): PrettyCommand {
75
+ const words = (raw ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
76
+ if (words.length === 0) return { kind: "status" };
77
+
78
+ const [head, ...rest] = words;
79
+ if (head === "status" || head === "list") return { kind: "status" };
80
+ if (head === "reset") return { kind: "reset" };
81
+
82
+ if (head === "on" || head === "off") {
83
+ const { tools, unknown } = resolveTools(rest.length > 0 ? rest : ["all"]);
84
+ if (unknown.length > 0) return { kind: "error", message: `Unknown tool "${unknown[0]}". ${PRETTY_USAGE}` };
85
+ return { kind: "set", tools, on: head === "on" };
86
+ }
87
+
88
+ const { tools, unknown } = resolveTools(words);
89
+ if (unknown.length > 0) return { kind: "error", message: `Unknown tool "${unknown[0]}". ${PRETTY_USAGE}` };
90
+ return { kind: "toggle", tools };
91
+ }
92
+
93
+ /** Apply a parsed command; returns the new config and the tools that changed. */
94
+ export function applyCommand(
95
+ config: PrettyConfig,
96
+ command: PrettyCommand,
97
+ ): { config: PrettyConfig; changed: PrettyTool[] } {
98
+ switch (command.kind) {
99
+ case "toggle": {
100
+ let next = config;
101
+ for (const tool of command.tools) next = toggleTool(next, tool);
102
+ return { config: next, changed: command.tools };
103
+ }
104
+ case "set": {
105
+ const disabled = command.on
106
+ ? config.disabled.filter((t) => !command.tools.includes(t))
107
+ : [...new Set([...config.disabled, ...command.tools])];
108
+ const changed = command.tools.filter((t) => config.disabled.includes(t) !== disabled.includes(t));
109
+ return { config: { disabled }, changed };
110
+ }
111
+ case "reset":
112
+ return { config: DEFAULT_CONFIG, changed: [...config.disabled] };
113
+ default:
114
+ return { config, changed: [] };
115
+ }
116
+ }
117
+
36
118
  export function statusLines(config: PrettyConfig): string[] {
37
119
  return PRETTY_TOOLS.map(
38
120
  (tool) => `${config.disabled.includes(tool) ? "○" : "●"} ${tool}`,
package/src/diff.ts CHANGED
@@ -5,12 +5,21 @@ export interface DiffStats {
5
5
  removed: number;
6
6
  }
7
7
 
8
- /** Count +/− lines in a display diff (ignores headers like +++/---). */
8
+ /**
9
+ * Count +/− lines in a display diff. File headers (+++/---) are skipped, but
10
+ * only where they can actually appear — before the first hunk. Inside a hunk,
11
+ * a removed line whose content starts with `--` is a real removal.
12
+ */
9
13
  export function diffStats(diff: string): DiffStats {
10
14
  let added = 0;
11
15
  let removed = 0;
16
+ let inHunk = false;
12
17
  for (const line of diff.split("\n")) {
13
- if (line.startsWith("+++") || line.startsWith("---")) continue;
18
+ if (line.startsWith("@@")) {
19
+ inHunk = true;
20
+ continue;
21
+ }
22
+ if (!inHunk && (line.startsWith("+++") || line.startsWith("---"))) continue;
14
23
  if (line.startsWith("+")) added++;
15
24
  else if (line.startsWith("-")) removed++;
16
25
  }
package/src/preview.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * One capping primitive for every body the renderers print. Collapsed rows get
3
+ * a short peek; expanded rows get a generous but bounded view — a 5,000-line
4
+ * diff or grep result should not be able to scroll the whole conversation out
5
+ * of the terminal just because someone pressed expand.
6
+ */
7
+
8
+ export interface PreviewLimits {
9
+ collapsed: number;
10
+ expanded: number;
11
+ }
12
+
13
+ export const DEFAULT_LIMITS: PreviewLimits = { collapsed: 12, expanded: 200 };
14
+
15
+ export function preview(text: string, expanded: boolean, limits: PreviewLimits = DEFAULT_LIMITS): string {
16
+ const max = expanded ? limits.expanded : limits.collapsed;
17
+ const lines = text.split("\n");
18
+ if (lines.length <= max) return text;
19
+ const hidden = lines.length - max;
20
+ return `${lines.slice(0, max).join("\n")}\n… +${hidden} more line${hidden === 1 ? "" : "s"}`;
21
+ }