@pify/pretty 0.2.0 → 0.3.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
@@ -34,6 +34,22 @@ Aliases are accepted where they're obvious: `list`/`dir` → `ls`, `search`/`rg`
34
34
 
35
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
36
 
37
+ ## Settings (v0.3)
38
+
39
+ Every cap in a renderer is somebody's taste, and the right number depends on your terminal. Put them in `.pi/pretty.json` (project) or `<agentDir>/pretty.json` (global — the project file wins):
40
+
41
+ ```json
42
+ {
43
+ "collapsedLines": 12,
44
+ "expandedLines": 200,
45
+ "diffLines": 200,
46
+ "syntaxHighlight": true,
47
+ "summaryClip": 100
48
+ }
49
+ ```
50
+
51
+ Unknown keys, wrong types, and absurd numbers are reported at session start and fall back to the shipped defaults rather than taking the renderers down — a typo tells you it was a typo instead of quietly doing nothing. `/pretty` shows the settings in force and where they came from. (The case for making these configurable is [`pi-cc-extensions`](https://github.com/minuque/pi-cc-extensions)' `/ccstyle`, which does it with a whole settings panel.)
52
+
37
53
  ## How it works
38
54
 
39
55
  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.
@@ -21,12 +21,15 @@ import {
21
21
  createLsTool,
22
22
  createReadTool,
23
23
  createWriteTool,
24
+ getAgentDir,
24
25
  getLanguageFromPath,
25
26
  highlightCode,
26
27
  type ExtensionAPI,
27
28
  type ExtensionContext,
28
29
  } from "@earendil-works/pi-coding-agent";
29
30
  import { Text } from "@earendil-works/pi-tui";
31
+ import { readFileSync } from "node:fs";
32
+ import { join } from "node:path";
30
33
 
31
34
  import {
32
35
  PRETTY_CONFIG,
@@ -37,7 +40,8 @@ import {
37
40
  statusLines,
38
41
  } from "../src/config.ts";
39
42
  import { colorizeDiff, diffStats, statsLabel } from "../src/diff.ts";
40
- import { preview } from "../src/preview.ts";
43
+ import { limitsFrom, preview } from "../src/preview.ts";
44
+ import { DEFAULT_SETTINGS, formatSettings, resolveSettings, type PrettySettings } from "../src/settings.ts";
41
45
  import {
42
46
  bashCall,
43
47
  bashSummary,
@@ -68,6 +72,36 @@ type AnyTool = {
68
72
 
69
73
  export default function pretty(pi: ExtensionAPI) {
70
74
  let config: PrettyConfig = DEFAULT_CONFIG;
75
+ let settings: PrettySettings = DEFAULT_SETTINGS;
76
+ let settingsSource: string | null = null;
77
+ let settingsWarnings: string[] = [];
78
+
79
+ /** Project settings win over global ones; neither is required. */
80
+ function loadSettings(cwd: string): void {
81
+ const candidates = [join(cwd, ".pi", "pretty.json"), join(getAgentDir(), "pretty.json")];
82
+ for (const file of candidates) {
83
+ let raw: string;
84
+ try {
85
+ raw = readFileSync(file, "utf8");
86
+ } catch {
87
+ continue;
88
+ }
89
+ try {
90
+ const parsed = resolveSettings(JSON.parse(raw));
91
+ settings = parsed.settings;
92
+ settingsWarnings = parsed.warnings;
93
+ settingsSource = file;
94
+ } catch (err) {
95
+ settings = DEFAULT_SETTINGS;
96
+ settingsSource = null;
97
+ settingsWarnings = [`${file}: ${err instanceof Error ? err.message : String(err)}`];
98
+ }
99
+ return;
100
+ }
101
+ settings = DEFAULT_SETTINGS;
102
+ settingsSource = null;
103
+ settingsWarnings = [];
104
+ }
71
105
  let originals: Record<PrettyTool, AnyTool> | null = null;
72
106
 
73
107
  function isFailed(result: unknown): boolean {
@@ -132,11 +166,11 @@ export default function pretty(pi: ExtensionAPI) {
132
166
  ) => {
133
167
  const output = textContent(result);
134
168
  if (options.isPartial) {
135
- return new Text(`${theme.fg("warning", "Running…")}\n${preview(output, false)}`, 0, 0);
169
+ return new Text(`${theme.fg("warning", "Running…")}\n${preview(output, false, limitsFrom(settings))}`, 0, 0);
136
170
  }
137
171
  const failed = isFailed(result);
138
172
  const summary = bashSummary(theme, output, failed);
139
- const body = output && (options.expanded || failed) ? `\n${preview(output, options.expanded === true)}` : "";
173
+ const body = output && (options.expanded || failed) ? `\n${preview(output, options.expanded === true, limitsFrom(settings))}` : "";
140
174
  return new Text(summary + body, 0, 0);
141
175
  },
142
176
  };
@@ -158,7 +192,7 @@ export default function pretty(pi: ExtensionAPI) {
158
192
  : "";
159
193
  const stats = statsLabel(theme, diffStats(diff));
160
194
  if (!options.expanded) return new Text(stats, 0, 0);
161
- return new Text(`${stats}\n${colorizeDiff(theme, preview(diff, true))}`, 0, 0);
195
+ return new Text(`${stats}\n${colorizeDiff(theme, preview(diff, true, { collapsed: settings.collapsedLines, expanded: settings.diffLines }))}`, 0, 0);
162
196
  },
163
197
  };
164
198
  case "write":
@@ -199,7 +233,7 @@ export default function pretty(pi: ExtensionAPI) {
199
233
  tool === "grep" ? { one: "match", many: "matches" } : { one: "result", many: "results" },
200
234
  );
201
235
  if (!options.expanded || failed || !output) return new Text(summary, 0, 0);
202
- return new Text(`${summary}\n${preview(output, true)}`, 0, 0);
236
+ return new Text(`${summary}\n${preview(output, true, limitsFrom(settings))}`, 0, 0);
203
237
  },
204
238
  };
205
239
  case "ls":
@@ -215,7 +249,7 @@ export default function pretty(pi: ExtensionAPI) {
215
249
  const failed = isFailed(result);
216
250
  const summary = matchSummary(theme, output, failed, { one: "entry", many: "entries" });
217
251
  if (!options.expanded || failed) return new Text(summary, 0, 0);
218
- return new Text(`${summary}\n${preview(output, true)}`, 0, 0);
252
+ return new Text(`${summary}\n${preview(output, true, limitsFrom(settings))}`, 0, 0);
219
253
  },
220
254
  };
221
255
  }
@@ -241,8 +275,12 @@ export default function pretty(pi: ExtensionAPI) {
241
275
 
242
276
  pi.on("session_start", async (_event, ctx) => {
243
277
  originals = buildOriginals(ctx.cwd);
278
+ loadSettings(ctx.cwd);
244
279
  config = replayBranch(ctx.sessionManager.getBranch() as never);
245
280
  applyAll();
281
+ if (settingsWarnings.length > 0 && ctx.hasUI) {
282
+ ctx.ui.notify(`pretty settings: ${settingsWarnings.join("; ")}`, "warning");
283
+ }
246
284
  });
247
285
 
248
286
  pi.on("session_tree", async (_event, ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pify/pretty",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",
@@ -53,8 +53,8 @@
53
53
  }
54
54
  },
55
55
  "devDependencies": {
56
- "@earendil-works/pi-coding-agent": "^0.84.4",
57
- "@earendil-works/pi-tui": "^0.84.4",
56
+ "@earendil-works/pi-coding-agent": "^0.85.1",
57
+ "@earendil-works/pi-tui": "^0.85.1",
58
58
  "@types/node": "^22.10.2",
59
59
  "typescript": "^5.7.2"
60
60
  },
package/src/config.ts CHANGED
@@ -33,7 +33,10 @@ 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";
36
+ export const PRETTY_USAGE = [
37
+ "Usage: /pretty [status | on|off [tool…] | reset | <tool…>] tools: read, bash, edit, write, grep, find, ls",
38
+ "Settings: .pi/pretty.json (project) or <agentDir>/pretty.json",
39
+ ].join("\n");
37
40
 
38
41
  /** Aliases the model or a hurried user is likely to type. */
39
42
  const ALIASES: Record<string, PrettyTool> = {
package/src/preview.ts CHANGED
@@ -12,6 +12,11 @@ export interface PreviewLimits {
12
12
 
13
13
  export const DEFAULT_LIMITS: PreviewLimits = { collapsed: 12, expanded: 200 };
14
14
 
15
+ /** Limits from user settings (v0.3); the caps themselves are unchanged. */
16
+ export function limitsFrom(settings: { collapsedLines: number; expandedLines: number }): PreviewLimits {
17
+ return { collapsed: settings.collapsedLines, expanded: settings.expandedLines };
18
+ }
19
+
15
20
  export function preview(text: string, expanded: boolean, limits: PreviewLimits = DEFAULT_LIMITS): string {
16
21
  const max = expanded ? limits.expanded : limits.collapsed;
17
22
  const lines = text.split("\n");
@@ -0,0 +1,93 @@
1
+ /**
2
+ * User settings for the renderers.
3
+ *
4
+ * pi-cc-extensions makes the case by example: every number in a renderer is
5
+ * someone's taste, and the ones that matter are the caps. A 200-line expanded
6
+ * body is right on a 4K monitor and absurd in a 24-row terminal, and nobody
7
+ * wants to fork a package to change an integer.
8
+ *
9
+ * Read from `.pi/pretty.json` in the project, else `<agentDir>/pretty.json`.
10
+ * Unknown keys are ignored and bad values fall back to the default, so an
11
+ * old or hand-edited file can never take the renderers down — the worst case
12
+ * is the shipped defaults, which is where everyone starts anyway.
13
+ */
14
+
15
+ export interface PrettySettings {
16
+ /** Lines shown in a collapsed body (bash streaming, tool output). */
17
+ collapsedLines: number;
18
+ /** Lines shown after expanding, before "+N more lines". */
19
+ expandedLines: number;
20
+ /** Lines of an edit diff shown after expanding. */
21
+ diffLines: number;
22
+ /** Highlight expanded read results with pi's own highlighter. */
23
+ syntaxHighlight: boolean;
24
+ /** Longest path/command shown in a one-line summary. */
25
+ summaryClip: number;
26
+ }
27
+
28
+ export const DEFAULT_SETTINGS: PrettySettings = {
29
+ collapsedLines: 12,
30
+ expandedLines: 200,
31
+ diffLines: 200,
32
+ syntaxHighlight: true,
33
+ summaryClip: 100,
34
+ };
35
+
36
+ const LIMITS: Record<keyof PrettySettings, { min: number; max: number } | null> = {
37
+ collapsedLines: { min: 1, max: 200 },
38
+ expandedLines: { min: 5, max: 10_000 },
39
+ diffLines: { min: 5, max: 10_000 },
40
+ syntaxHighlight: null,
41
+ summaryClip: { min: 20, max: 500 },
42
+ };
43
+
44
+ /**
45
+ * Merge a parsed settings file over the defaults. Returns the settings that
46
+ * will actually be used plus a note for every value that was rejected —
47
+ * silently ignoring a typo is how someone spends an afternoon wondering why
48
+ * their config does nothing.
49
+ */
50
+ export function resolveSettings(raw: unknown): { settings: PrettySettings; warnings: string[] } {
51
+ const settings: PrettySettings = { ...DEFAULT_SETTINGS };
52
+ const warnings: string[] = [];
53
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
54
+ if (raw !== undefined && raw !== null) warnings.push("settings file is not an object — ignored");
55
+ return { settings, warnings };
56
+ }
57
+
58
+ for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
59
+ if (!(key in DEFAULT_SETTINGS)) {
60
+ warnings.push(`unknown setting "${key}"`);
61
+ continue;
62
+ }
63
+ const name = key as keyof PrettySettings;
64
+ const limit = LIMITS[name];
65
+ if (limit === null) {
66
+ if (typeof value !== "boolean") {
67
+ warnings.push(`"${key}" must be true or false — using ${String(DEFAULT_SETTINGS[name])}`);
68
+ continue;
69
+ }
70
+ (settings[name] as boolean) = value;
71
+ continue;
72
+ }
73
+ if (typeof value !== "number" || !Number.isFinite(value)) {
74
+ warnings.push(`"${key}" must be a number — using ${String(DEFAULT_SETTINGS[name])}`);
75
+ continue;
76
+ }
77
+ const clamped = Math.round(Math.min(limit.max, Math.max(limit.min, value)));
78
+ if (clamped !== value) {
79
+ warnings.push(`"${key}" clamped to ${clamped} (allowed ${limit.min}–${limit.max})`);
80
+ }
81
+ (settings[name] as number) = clamped;
82
+ }
83
+
84
+ return { settings, warnings };
85
+ }
86
+
87
+ export function formatSettings(settings: PrettySettings, source: string | null): string {
88
+ const width = Object.keys(settings).reduce((m, k) => Math.max(m, k.length), 0);
89
+ const lines = Object.entries(settings).map(
90
+ ([key, value]) => ` ${key.padEnd(width)} ${String(value)}`,
91
+ );
92
+ return [source ? `Settings from ${source}` : "Settings (defaults — no pretty.json found)", ...lines].join("\n");
93
+ }