@pify/pretty 0.1.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 +26 -2
- package/extensions/pretty.ts +71 -26
- package/package.json +12 -7
- package/src/config.ts +85 -0
- package/src/diff.ts +11 -2
- package/src/preview.ts +26 -0
- package/src/settings.ts +93 -0
package/README.md
CHANGED
|
@@ -22,10 +22,34 @@ 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
|
|
26
|
-
/pretty bash
|
|
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
|
+
|
|
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
|
+
|
|
29
53
|
## How it works
|
|
30
54
|
|
|
31
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.
|
package/extensions/pretty.ts
CHANGED
|
@@ -21,15 +21,27 @@ 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
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
PRETTY_CONFIG,
|
|
36
|
+
PRETTY_USAGE,
|
|
37
|
+
applyCommand,
|
|
38
|
+
parsePrettyCommand,
|
|
39
|
+
replayBranch,
|
|
40
|
+
statusLines,
|
|
41
|
+
} from "../src/config.ts";
|
|
32
42
|
import { colorizeDiff, diffStats, statsLabel } from "../src/diff.ts";
|
|
43
|
+
import { limitsFrom, preview } from "../src/preview.ts";
|
|
44
|
+
import { DEFAULT_SETTINGS, formatSettings, resolveSettings, type PrettySettings } from "../src/settings.ts";
|
|
33
45
|
import {
|
|
34
46
|
bashCall,
|
|
35
47
|
bashSummary,
|
|
@@ -50,8 +62,6 @@ import {
|
|
|
50
62
|
type ThemeLike,
|
|
51
63
|
} from "../src/types.ts";
|
|
52
64
|
|
|
53
|
-
const PREVIEW_LINES = 12;
|
|
54
|
-
|
|
55
65
|
type AnyTool = {
|
|
56
66
|
name: string;
|
|
57
67
|
description: string;
|
|
@@ -62,19 +72,42 @@ type AnyTool = {
|
|
|
62
72
|
|
|
63
73
|
export default function pretty(pi: ExtensionAPI) {
|
|
64
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
|
+
}
|
|
65
105
|
let originals: Record<PrettyTool, AnyTool> | null = null;
|
|
66
106
|
|
|
67
107
|
function isFailed(result: unknown): boolean {
|
|
68
108
|
return isRecord(result) && result.isError === true;
|
|
69
109
|
}
|
|
70
110
|
|
|
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
111
|
function buildOriginals(cwd: string): Record<PrettyTool, AnyTool> {
|
|
79
112
|
return {
|
|
80
113
|
read: createReadTool(cwd) as unknown as AnyTool,
|
|
@@ -133,11 +166,11 @@ export default function pretty(pi: ExtensionAPI) {
|
|
|
133
166
|
) => {
|
|
134
167
|
const output = textContent(result);
|
|
135
168
|
if (options.isPartial) {
|
|
136
|
-
return new Text(`${theme.fg("warning", "Running…")}\n${
|
|
169
|
+
return new Text(`${theme.fg("warning", "Running…")}\n${preview(output, false, limitsFrom(settings))}`, 0, 0);
|
|
137
170
|
}
|
|
138
171
|
const failed = isFailed(result);
|
|
139
172
|
const summary = bashSummary(theme, output, failed);
|
|
140
|
-
const body = output && (options.expanded || failed) ? `\n${
|
|
173
|
+
const body = output && (options.expanded || failed) ? `\n${preview(output, options.expanded === true, limitsFrom(settings))}` : "";
|
|
141
174
|
return new Text(summary + body, 0, 0);
|
|
142
175
|
},
|
|
143
176
|
};
|
|
@@ -159,7 +192,7 @@ export default function pretty(pi: ExtensionAPI) {
|
|
|
159
192
|
: "";
|
|
160
193
|
const stats = statsLabel(theme, diffStats(diff));
|
|
161
194
|
if (!options.expanded) return new Text(stats, 0, 0);
|
|
162
|
-
return new Text(`${stats}\n${colorizeDiff(theme, diff)}`, 0, 0);
|
|
195
|
+
return new Text(`${stats}\n${colorizeDiff(theme, preview(diff, true, { collapsed: settings.collapsedLines, expanded: settings.diffLines }))}`, 0, 0);
|
|
163
196
|
},
|
|
164
197
|
};
|
|
165
198
|
case "write":
|
|
@@ -200,7 +233,7 @@ export default function pretty(pi: ExtensionAPI) {
|
|
|
200
233
|
tool === "grep" ? { one: "match", many: "matches" } : { one: "result", many: "results" },
|
|
201
234
|
);
|
|
202
235
|
if (!options.expanded || failed || !output) return new Text(summary, 0, 0);
|
|
203
|
-
return new Text(`${summary}\n${
|
|
236
|
+
return new Text(`${summary}\n${preview(output, true, limitsFrom(settings))}`, 0, 0);
|
|
204
237
|
},
|
|
205
238
|
};
|
|
206
239
|
case "ls":
|
|
@@ -216,7 +249,7 @@ export default function pretty(pi: ExtensionAPI) {
|
|
|
216
249
|
const failed = isFailed(result);
|
|
217
250
|
const summary = matchSummary(theme, output, failed, { one: "entry", many: "entries" });
|
|
218
251
|
if (!options.expanded || failed) return new Text(summary, 0, 0);
|
|
219
|
-
return new Text(`${summary}\n${
|
|
252
|
+
return new Text(`${summary}\n${preview(output, true, limitsFrom(settings))}`, 0, 0);
|
|
220
253
|
},
|
|
221
254
|
};
|
|
222
255
|
}
|
|
@@ -242,8 +275,12 @@ export default function pretty(pi: ExtensionAPI) {
|
|
|
242
275
|
|
|
243
276
|
pi.on("session_start", async (_event, ctx) => {
|
|
244
277
|
originals = buildOriginals(ctx.cwd);
|
|
278
|
+
loadSettings(ctx.cwd);
|
|
245
279
|
config = replayBranch(ctx.sessionManager.getBranch() as never);
|
|
246
280
|
applyAll();
|
|
281
|
+
if (settingsWarnings.length > 0 && ctx.hasUI) {
|
|
282
|
+
ctx.ui.notify(`pretty settings: ${settingsWarnings.join("; ")}`, "warning");
|
|
283
|
+
}
|
|
247
284
|
});
|
|
248
285
|
|
|
249
286
|
pi.on("session_tree", async (_event, ctx) => {
|
|
@@ -254,23 +291,31 @@ export default function pretty(pi: ExtensionAPI) {
|
|
|
254
291
|
// ── Command ──────────────────────────────────────────────────────────
|
|
255
292
|
|
|
256
293
|
pi.registerCommand("pretty", {
|
|
257
|
-
description: "
|
|
294
|
+
description: "Pretty tool rendering: /pretty [status | on|off [tool…] | reset | <tool…>]",
|
|
258
295
|
handler: async (args, ctx: ExtensionContext) => {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
ctx.ui.notify(`Pretty renderers\n${statusLines(config).join("\n")}\nToggle with /pretty <tool>`, "info");
|
|
296
|
+
const command = parsePrettyCommand(args ?? "");
|
|
297
|
+
if (command.kind === "error") {
|
|
298
|
+
if (ctx.hasUI) ctx.ui.notify(command.message, "warning");
|
|
263
299
|
return;
|
|
264
300
|
}
|
|
265
|
-
if (
|
|
266
|
-
ctx.
|
|
301
|
+
if (command.kind === "status") {
|
|
302
|
+
if (ctx.hasUI) {
|
|
303
|
+
ctx.ui.notify(`Pretty renderers\n${statusLines(config).join("\n")}\n${PRETTY_USAGE}`, "info");
|
|
304
|
+
}
|
|
267
305
|
return;
|
|
268
306
|
}
|
|
269
|
-
|
|
307
|
+
|
|
308
|
+
const { config: next, changed } = applyCommand(config, command);
|
|
309
|
+
config = next;
|
|
270
310
|
pi.appendEntry(PRETTY_CONFIG, config);
|
|
271
|
-
applyTool(
|
|
272
|
-
|
|
273
|
-
ctx.ui.notify(
|
|
311
|
+
for (const tool of changed) applyTool(tool);
|
|
312
|
+
if (!ctx.hasUI) return;
|
|
313
|
+
ctx.ui.notify(
|
|
314
|
+
changed.length === 0
|
|
315
|
+
? "Nothing changed."
|
|
316
|
+
: changed.map((t) => `pretty ${t}: ${config.disabled.includes(t) ? "off" : "on"}`).join("\n"),
|
|
317
|
+
"info",
|
|
318
|
+
);
|
|
274
319
|
},
|
|
275
320
|
});
|
|
276
321
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pify/pretty",
|
|
3
|
-
"version": "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",
|
|
@@ -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": [
|
|
34
|
+
"extensions": [
|
|
35
|
+
"./extensions/pretty.ts"
|
|
36
|
+
]
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
39
|
"typecheck": "tsc --noEmit",
|
|
@@ -44,12 +45,16 @@
|
|
|
44
45
|
"@earendil-works/pi-tui": "*"
|
|
45
46
|
},
|
|
46
47
|
"peerDependenciesMeta": {
|
|
47
|
-
"@earendil-works/pi-coding-agent": {
|
|
48
|
-
|
|
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
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
52
|
-
"@earendil-works/pi-tui": "^0.
|
|
56
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
57
|
+
"@earendil-works/pi-tui": "^0.85.1",
|
|
53
58
|
"@types/node": "^22.10.2",
|
|
54
59
|
"typescript": "^5.7.2"
|
|
55
60
|
},
|
package/src/config.ts
CHANGED
|
@@ -33,6 +33,91 @@ export function toggleTool(config: PrettyConfig, tool: PrettyTool): PrettyConfig
|
|
|
33
33
|
: { disabled: [...config.disabled, tool] };
|
|
34
34
|
}
|
|
35
35
|
|
|
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");
|
|
40
|
+
|
|
41
|
+
/** Aliases the model or a hurried user is likely to type. */
|
|
42
|
+
const ALIASES: Record<string, PrettyTool> = {
|
|
43
|
+
list: "ls",
|
|
44
|
+
dir: "ls",
|
|
45
|
+
search: "grep",
|
|
46
|
+
rg: "grep",
|
|
47
|
+
cat: "read",
|
|
48
|
+
shell: "bash",
|
|
49
|
+
sh: "bash",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type PrettyCommand =
|
|
53
|
+
| { kind: "status" }
|
|
54
|
+
| { kind: "toggle"; tools: PrettyTool[] }
|
|
55
|
+
| { kind: "set"; tools: PrettyTool[]; on: boolean }
|
|
56
|
+
| { kind: "reset" }
|
|
57
|
+
| { kind: "error"; message: string };
|
|
58
|
+
|
|
59
|
+
function resolveTools(words: string[]): { tools: PrettyTool[]; unknown: string[] } {
|
|
60
|
+
const tools: PrettyTool[] = [];
|
|
61
|
+
const unknown: string[] = [];
|
|
62
|
+
for (const word of words) {
|
|
63
|
+
if (word === "all") {
|
|
64
|
+
tools.push(...PRETTY_TOOLS);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const name = ALIASES[word] ?? word;
|
|
68
|
+
if (isPrettyTool(name)) {
|
|
69
|
+
if (!tools.includes(name)) tools.push(name);
|
|
70
|
+
} else {
|
|
71
|
+
unknown.push(word);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { tools, unknown };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function parsePrettyCommand(raw: string): PrettyCommand {
|
|
78
|
+
const words = (raw ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
79
|
+
if (words.length === 0) return { kind: "status" };
|
|
80
|
+
|
|
81
|
+
const [head, ...rest] = words;
|
|
82
|
+
if (head === "status" || head === "list") return { kind: "status" };
|
|
83
|
+
if (head === "reset") return { kind: "reset" };
|
|
84
|
+
|
|
85
|
+
if (head === "on" || head === "off") {
|
|
86
|
+
const { tools, unknown } = resolveTools(rest.length > 0 ? rest : ["all"]);
|
|
87
|
+
if (unknown.length > 0) return { kind: "error", message: `Unknown tool "${unknown[0]}". ${PRETTY_USAGE}` };
|
|
88
|
+
return { kind: "set", tools, on: head === "on" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { tools, unknown } = resolveTools(words);
|
|
92
|
+
if (unknown.length > 0) return { kind: "error", message: `Unknown tool "${unknown[0]}". ${PRETTY_USAGE}` };
|
|
93
|
+
return { kind: "toggle", tools };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Apply a parsed command; returns the new config and the tools that changed. */
|
|
97
|
+
export function applyCommand(
|
|
98
|
+
config: PrettyConfig,
|
|
99
|
+
command: PrettyCommand,
|
|
100
|
+
): { config: PrettyConfig; changed: PrettyTool[] } {
|
|
101
|
+
switch (command.kind) {
|
|
102
|
+
case "toggle": {
|
|
103
|
+
let next = config;
|
|
104
|
+
for (const tool of command.tools) next = toggleTool(next, tool);
|
|
105
|
+
return { config: next, changed: command.tools };
|
|
106
|
+
}
|
|
107
|
+
case "set": {
|
|
108
|
+
const disabled = command.on
|
|
109
|
+
? config.disabled.filter((t) => !command.tools.includes(t))
|
|
110
|
+
: [...new Set([...config.disabled, ...command.tools])];
|
|
111
|
+
const changed = command.tools.filter((t) => config.disabled.includes(t) !== disabled.includes(t));
|
|
112
|
+
return { config: { disabled }, changed };
|
|
113
|
+
}
|
|
114
|
+
case "reset":
|
|
115
|
+
return { config: DEFAULT_CONFIG, changed: [...config.disabled] };
|
|
116
|
+
default:
|
|
117
|
+
return { config, changed: [] };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
36
121
|
export function statusLines(config: PrettyConfig): string[] {
|
|
37
122
|
return PRETTY_TOOLS.map(
|
|
38
123
|
(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
|
-
/**
|
|
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("
|
|
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,26 @@
|
|
|
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
|
+
/** 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
|
+
|
|
20
|
+
export function preview(text: string, expanded: boolean, limits: PreviewLimits = DEFAULT_LIMITS): string {
|
|
21
|
+
const max = expanded ? limits.expanded : limits.collapsed;
|
|
22
|
+
const lines = text.split("\n");
|
|
23
|
+
if (lines.length <= max) return text;
|
|
24
|
+
const hidden = lines.length - max;
|
|
25
|
+
return `${lines.slice(0, max).join("\n")}\n… +${hidden} more line${hidden === 1 ? "" : "s"}`;
|
|
26
|
+
}
|
package/src/settings.ts
ADDED
|
@@ -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
|
+
}
|