@pify/pretty 0.1.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/LICENSE +21 -0
- package/README.md +37 -0
- package/extensions/pretty.ts +276 -0
- package/package.json +59 -0
- package/src/config.ts +40 -0
- package/src/diff.ts +36 -0
- package/src/summary.ts +77 -0
- package/src/types.ts +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pifydev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @pify/pretty
|
|
2
|
+
|
|
3
|
+
Compact, theme-aware rendering for [pi](https://github.com/earendil-works/pi)'s built-in tools — one-line summaries that expand on demand, syntax-highlighted reads, colorized diffs. **Behavior is untouched**: every tool delegates to pi's original implementation; only the rendering changes.
|
|
4
|
+
|
|
5
|
+
Part of the [Pify suite](https://github.com/pifydev). Install with [`pify install pretty`](https://github.com/pifydev/cli) or `pi install npm:@pify/pretty`.
|
|
6
|
+
|
|
7
|
+
## What you see
|
|
8
|
+
|
|
9
|
+
| Tool | Collapsed | Expanded |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `read` | `Read src/app.ts · lines 1–50` → `50 lines` | Syntax-highlighted content (pi's own highlighter, theme-matched) |
|
|
12
|
+
| `bash` | `Bash npm test` → `✓ 12 output lines` / `✗ first error line` | Output (12-line preview while running) |
|
|
13
|
+
| `edit` | `Edit src/app.ts` → `+12 -3` | Colorized diff |
|
|
14
|
+
| `write` | `Write y.md · 34 lines` → `✓ written` | — |
|
|
15
|
+
| `grep`/`find` | `Grep TODO in src` → `7 matches` | Match list |
|
|
16
|
+
| `ls` | `List packages` → `23 entries` | Listing |
|
|
17
|
+
|
|
18
|
+
Expand with pi's standard toggle (Ctrl+O on a tool block). Failures always show the first error line in the error color.
|
|
19
|
+
|
|
20
|
+
## Per-tool opt-out
|
|
21
|
+
|
|
22
|
+
Each renderer toggles independently (persisted per session):
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
/pretty # show which renderers are on
|
|
26
|
+
/pretty bash # toggle one back to pi's default rendering
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
Only one extension can own a tool's rendering: remove other pretty/TUI tool-renderer extensions (e.g. `pi-pretty`, `pi-pretty-tui`) before installing.
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
MIT © [Pify maintainers](https://github.com/pifydev)
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pify/pretty — compact, theme-aware rendering for pi's built-in tools.
|
|
3
|
+
*
|
|
4
|
+
* Pure rendering: every tool is re-registered with its ORIGINAL execute
|
|
5
|
+
* delegated untouched (pi's official built-in-tool-renderer pattern); only
|
|
6
|
+
* renderCall/renderResult change. Collapsed one-line summaries expand with
|
|
7
|
+
* pi's standard toggle; read results get syntax highlighting via pi's own
|
|
8
|
+
* highlightCode; edit diffs are colorized with +N −M stats. Each renderer
|
|
9
|
+
* toggles independently with /pretty <tool> (zentui's opt-in principle),
|
|
10
|
+
* persisted per session.
|
|
11
|
+
*
|
|
12
|
+
* Design synthesis: compact summary shapes (ykn0309/pi-pretty-tui),
|
|
13
|
+
* delegate-execute renderer pattern (pi examples), theme-aware minimalism
|
|
14
|
+
* (giladbarnea/pi-pretty-bash), per-surface opt-in (pi-zentui).
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
createBashTool,
|
|
18
|
+
createEditTool,
|
|
19
|
+
createFindTool,
|
|
20
|
+
createGrepTool,
|
|
21
|
+
createLsTool,
|
|
22
|
+
createReadTool,
|
|
23
|
+
createWriteTool,
|
|
24
|
+
getLanguageFromPath,
|
|
25
|
+
highlightCode,
|
|
26
|
+
type ExtensionAPI,
|
|
27
|
+
type ExtensionContext,
|
|
28
|
+
} from "@earendil-works/pi-coding-agent";
|
|
29
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
30
|
+
|
|
31
|
+
import { PRETTY_CONFIG, isPrettyTool, replayBranch, statusLines, toggleTool } from "../src/config.ts";
|
|
32
|
+
import { colorizeDiff, diffStats, statsLabel } from "../src/diff.ts";
|
|
33
|
+
import {
|
|
34
|
+
bashCall,
|
|
35
|
+
bashSummary,
|
|
36
|
+
editCall,
|
|
37
|
+
listCall,
|
|
38
|
+
matchSummary,
|
|
39
|
+
readCall,
|
|
40
|
+
readSummary,
|
|
41
|
+
searchCall,
|
|
42
|
+
writeCall,
|
|
43
|
+
} from "../src/summary.ts";
|
|
44
|
+
import {
|
|
45
|
+
DEFAULT_CONFIG,
|
|
46
|
+
isRecord,
|
|
47
|
+
textContent,
|
|
48
|
+
type PrettyConfig,
|
|
49
|
+
type PrettyTool,
|
|
50
|
+
type ThemeLike,
|
|
51
|
+
} from "../src/types.ts";
|
|
52
|
+
|
|
53
|
+
const PREVIEW_LINES = 12;
|
|
54
|
+
|
|
55
|
+
type AnyTool = {
|
|
56
|
+
name: string;
|
|
57
|
+
description: string;
|
|
58
|
+
parameters: unknown;
|
|
59
|
+
execute: (...args: never[]) => unknown;
|
|
60
|
+
[key: string]: unknown;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default function pretty(pi: ExtensionAPI) {
|
|
64
|
+
let config: PrettyConfig = DEFAULT_CONFIG;
|
|
65
|
+
let originals: Record<PrettyTool, AnyTool> | null = null;
|
|
66
|
+
|
|
67
|
+
function isFailed(result: unknown): boolean {
|
|
68
|
+
return isRecord(result) && result.isError === true;
|
|
69
|
+
}
|
|
70
|
+
|
|
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
|
+
function buildOriginals(cwd: string): Record<PrettyTool, AnyTool> {
|
|
79
|
+
return {
|
|
80
|
+
read: createReadTool(cwd) as unknown as AnyTool,
|
|
81
|
+
bash: createBashTool(cwd) as unknown as AnyTool,
|
|
82
|
+
edit: createEditTool(cwd) as unknown as AnyTool,
|
|
83
|
+
write: createWriteTool(cwd) as unknown as AnyTool,
|
|
84
|
+
grep: createGrepTool(cwd) as unknown as AnyTool,
|
|
85
|
+
find: createFindTool(cwd) as unknown as AnyTool,
|
|
86
|
+
ls: createLsTool(cwd) as unknown as AnyTool,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Renderers per tool; delegate execution to the original untouched. */
|
|
91
|
+
function renderersFor(tool: PrettyTool): Record<string, unknown> {
|
|
92
|
+
switch (tool) {
|
|
93
|
+
case "read":
|
|
94
|
+
return {
|
|
95
|
+
renderCall: (args: { path?: string; offset?: number; limit?: number }, theme: ThemeLike) =>
|
|
96
|
+
new Text(readCall(theme, args ?? {}), 0, 0),
|
|
97
|
+
renderResult: (
|
|
98
|
+
result: unknown,
|
|
99
|
+
options: { expanded?: boolean; isPartial?: boolean },
|
|
100
|
+
theme: ThemeLike,
|
|
101
|
+
) => {
|
|
102
|
+
if (options.isPartial) return new Text(theme.fg("warning", "Reading…"), 0, 0);
|
|
103
|
+
const output = textContent(result);
|
|
104
|
+
const failed = isFailed(result);
|
|
105
|
+
const truncated =
|
|
106
|
+
isRecord(result) && isRecord(result.details) && isRecord(result.details.truncation)
|
|
107
|
+
? result.details.truncation.truncated === true
|
|
108
|
+
: false;
|
|
109
|
+
const summary = readSummary(theme, output, truncated, failed);
|
|
110
|
+
if (!options.expanded || failed) return new Text(summary, 0, 0);
|
|
111
|
+
const path =
|
|
112
|
+
isRecord(result) && isRecord(result.details) && typeof result.details.path === "string"
|
|
113
|
+
? result.details.path
|
|
114
|
+
: "";
|
|
115
|
+
const language = path ? getLanguageFromPath(path) : undefined;
|
|
116
|
+
let body = output;
|
|
117
|
+
try {
|
|
118
|
+
if (language) body = highlightCode(output, language).join("\n");
|
|
119
|
+
} catch {
|
|
120
|
+
// highlighting is best-effort
|
|
121
|
+
}
|
|
122
|
+
return new Text(`${summary}\n${body}`, 0, 0);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
case "bash":
|
|
126
|
+
return {
|
|
127
|
+
renderCall: (args: { command?: string }, theme: ThemeLike, context: { expanded?: boolean }) =>
|
|
128
|
+
new Text(bashCall(theme, args?.command ?? "", context?.expanded === true), 0, 0),
|
|
129
|
+
renderResult: (
|
|
130
|
+
result: unknown,
|
|
131
|
+
options: { expanded?: boolean; isPartial?: boolean },
|
|
132
|
+
theme: ThemeLike,
|
|
133
|
+
) => {
|
|
134
|
+
const output = textContent(result);
|
|
135
|
+
if (options.isPartial) {
|
|
136
|
+
return new Text(`${theme.fg("warning", "Running…")}\n${clip(output, false)}`, 0, 0);
|
|
137
|
+
}
|
|
138
|
+
const failed = isFailed(result);
|
|
139
|
+
const summary = bashSummary(theme, output, failed);
|
|
140
|
+
const body = output && (options.expanded || failed) ? `\n${clip(output, options.expanded === true)}` : "";
|
|
141
|
+
return new Text(summary + body, 0, 0);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
case "edit":
|
|
145
|
+
return {
|
|
146
|
+
renderCall: (args: { path?: string }, theme: ThemeLike) => new Text(editCall(theme, args ?? {}), 0, 0),
|
|
147
|
+
renderResult: (
|
|
148
|
+
result: unknown,
|
|
149
|
+
options: { expanded?: boolean; isPartial?: boolean },
|
|
150
|
+
theme: ThemeLike,
|
|
151
|
+
) => {
|
|
152
|
+
if (options.isPartial) return new Text(theme.fg("warning", "Editing…"), 0, 0);
|
|
153
|
+
if (isFailed(result)) {
|
|
154
|
+
return new Text(theme.fg("error", textContent(result).split("\n")[0] || "Edit failed"), 0, 0);
|
|
155
|
+
}
|
|
156
|
+
const diff =
|
|
157
|
+
isRecord(result) && isRecord(result.details) && typeof result.details.diff === "string"
|
|
158
|
+
? result.details.diff
|
|
159
|
+
: "";
|
|
160
|
+
const stats = statsLabel(theme, diffStats(diff));
|
|
161
|
+
if (!options.expanded) return new Text(stats, 0, 0);
|
|
162
|
+
return new Text(`${stats}\n${colorizeDiff(theme, diff)}`, 0, 0);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
case "write":
|
|
166
|
+
return {
|
|
167
|
+
renderCall: (args: { path?: string; content?: string }, theme: ThemeLike) =>
|
|
168
|
+
new Text(writeCall(theme, args ?? {}), 0, 0),
|
|
169
|
+
renderResult: (
|
|
170
|
+
result: unknown,
|
|
171
|
+
options: { expanded?: boolean; isPartial?: boolean },
|
|
172
|
+
theme: ThemeLike,
|
|
173
|
+
) => {
|
|
174
|
+
if (options.isPartial) return new Text(theme.fg("warning", "Writing…"), 0, 0);
|
|
175
|
+
if (isFailed(result)) {
|
|
176
|
+
return new Text(theme.fg("error", textContent(result).split("\n")[0] || "Write failed"), 0, 0);
|
|
177
|
+
}
|
|
178
|
+
return new Text(theme.fg("success", "✓ written"), 0, 0);
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
case "grep":
|
|
182
|
+
case "find":
|
|
183
|
+
return {
|
|
184
|
+
renderCall: (
|
|
185
|
+
args: { pattern?: string; path?: string; glob?: string },
|
|
186
|
+
theme: ThemeLike,
|
|
187
|
+
) => new Text(searchCall(theme, tool === "grep" ? "Grep" : "Find", args ?? {}), 0, 0),
|
|
188
|
+
renderResult: (
|
|
189
|
+
result: unknown,
|
|
190
|
+
options: { expanded?: boolean; isPartial?: boolean },
|
|
191
|
+
theme: ThemeLike,
|
|
192
|
+
) => {
|
|
193
|
+
if (options.isPartial) return new Text(theme.fg("warning", "Searching…"), 0, 0);
|
|
194
|
+
const output = textContent(result);
|
|
195
|
+
const failed = isFailed(result);
|
|
196
|
+
const summary = matchSummary(
|
|
197
|
+
theme,
|
|
198
|
+
output,
|
|
199
|
+
failed,
|
|
200
|
+
tool === "grep" ? { one: "match", many: "matches" } : { one: "result", many: "results" },
|
|
201
|
+
);
|
|
202
|
+
if (!options.expanded || failed || !output) return new Text(summary, 0, 0);
|
|
203
|
+
return new Text(`${summary}\n${clip(output, true)}`, 0, 0);
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
case "ls":
|
|
207
|
+
return {
|
|
208
|
+
renderCall: (args: { path?: string }, theme: ThemeLike) => new Text(listCall(theme, args ?? {}), 0, 0),
|
|
209
|
+
renderResult: (
|
|
210
|
+
result: unknown,
|
|
211
|
+
options: { expanded?: boolean; isPartial?: boolean },
|
|
212
|
+
theme: ThemeLike,
|
|
213
|
+
) => {
|
|
214
|
+
if (options.isPartial) return new Text(theme.fg("warning", "Listing…"), 0, 0);
|
|
215
|
+
const output = textContent(result);
|
|
216
|
+
const failed = isFailed(result);
|
|
217
|
+
const summary = matchSummary(theme, output, failed, { one: "entry", many: "entries" });
|
|
218
|
+
if (!options.expanded || failed) return new Text(summary, 0, 0);
|
|
219
|
+
return new Text(`${summary}\n${clip(output, true)}`, 0, 0);
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** (Re-)register one tool with or without pretty renderers. */
|
|
226
|
+
function applyTool(tool: PrettyTool): void {
|
|
227
|
+
if (!originals) return;
|
|
228
|
+
const original = originals[tool];
|
|
229
|
+
const withPretty = !config.disabled.includes(tool);
|
|
230
|
+
pi.registerTool({
|
|
231
|
+
...original,
|
|
232
|
+
...(withPretty ? renderersFor(tool) : {}),
|
|
233
|
+
} as never);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function applyAll(): void {
|
|
237
|
+
if (!originals) return;
|
|
238
|
+
for (const tool of Object.keys(originals) as PrettyTool[]) applyTool(tool);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Lifecycle ────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
244
|
+
originals = buildOriginals(ctx.cwd);
|
|
245
|
+
config = replayBranch(ctx.sessionManager.getBranch() as never);
|
|
246
|
+
applyAll();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
250
|
+
config = replayBranch(ctx.sessionManager.getBranch() as never);
|
|
251
|
+
applyAll();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ── Command ──────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
pi.registerCommand("pretty", {
|
|
257
|
+
description: "Toggle pretty tool rendering: /pretty [read|bash|edit|write|grep|find|ls]",
|
|
258
|
+
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");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (!isPrettyTool(target)) {
|
|
266
|
+
ctx.ui.notify(`Unknown tool "${target}". Tools: read, bash, edit, write, grep, find, ls`, "warning");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
config = toggleTool(config, target);
|
|
270
|
+
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");
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pify/pretty",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Compact, theme-aware rendering for pi's built-in tools: one-line summaries, syntax-highlighted reads, colorized diffs — behavior untouched",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"pi",
|
|
9
|
+
"pify",
|
|
10
|
+
"pretty",
|
|
11
|
+
"tui"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/pifydev/pretty#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/pifydev/pretty/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/pifydev/pretty.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Pify maintainers",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22.19.0"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"extensions",
|
|
29
|
+
"src",
|
|
30
|
+
"skills",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"pi": {
|
|
35
|
+
"extensions": ["./extensions/pretty.ts"]
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "bun test",
|
|
40
|
+
"prepublishOnly": "npm run typecheck && npm test"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
44
|
+
"@earendil-works/pi-tui": "*"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"@earendil-works/pi-coding-agent": { "optional": true },
|
|
48
|
+
"@earendil-works/pi-tui": { "optional": true }
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
52
|
+
"@earendil-works/pi-tui": "^0.84.4",
|
|
53
|
+
"@types/node": "^22.10.2",
|
|
54
|
+
"typescript": "^5.7.2"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CONFIG,
|
|
3
|
+
PRETTY_TOOLS,
|
|
4
|
+
isRecord,
|
|
5
|
+
type BranchEntryLike,
|
|
6
|
+
type PrettyConfig,
|
|
7
|
+
type PrettyTool,
|
|
8
|
+
} from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export const PRETTY_CONFIG = "pretty-config";
|
|
11
|
+
|
|
12
|
+
export function isPrettyTool(name: string): name is PrettyTool {
|
|
13
|
+
return (PRETTY_TOOLS as readonly string[]).includes(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Snapshot-based replay: last pretty-config entry on the branch wins. */
|
|
17
|
+
export function replayBranch(entries: BranchEntryLike[]): PrettyConfig {
|
|
18
|
+
let config = DEFAULT_CONFIG;
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (entry.type !== "custom" || entry.customType !== PRETTY_CONFIG) continue;
|
|
21
|
+
const data = entry.data;
|
|
22
|
+
if (!isRecord(data) || !Array.isArray(data.disabled)) continue;
|
|
23
|
+
config = {
|
|
24
|
+
disabled: data.disabled.filter((t): t is PrettyTool => typeof t === "string" && isPrettyTool(t)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function toggleTool(config: PrettyConfig, tool: PrettyTool): PrettyConfig {
|
|
31
|
+
return config.disabled.includes(tool)
|
|
32
|
+
? { disabled: config.disabled.filter((t) => t !== tool) }
|
|
33
|
+
: { disabled: [...config.disabled, tool] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function statusLines(config: PrettyConfig): string[] {
|
|
37
|
+
return PRETTY_TOOLS.map(
|
|
38
|
+
(tool) => `${config.disabled.includes(tool) ? "○" : "●"} ${tool}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
package/src/diff.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ThemeLike } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export interface DiffStats {
|
|
4
|
+
added: number;
|
|
5
|
+
removed: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Count +/− lines in a display diff (ignores headers like +++/---). */
|
|
9
|
+
export function diffStats(diff: string): DiffStats {
|
|
10
|
+
let added = 0;
|
|
11
|
+
let removed = 0;
|
|
12
|
+
for (const line of diff.split("\n")) {
|
|
13
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
14
|
+
if (line.startsWith("+")) added++;
|
|
15
|
+
else if (line.startsWith("-")) removed++;
|
|
16
|
+
}
|
|
17
|
+
return { added, removed };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function statsLabel(theme: ThemeLike, stats: DiffStats): string {
|
|
21
|
+
return `${theme.fg("success", `+${stats.added}`)} ${theme.fg("error", `-${stats.removed}`)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Colorize a display diff line-by-line with theme colors. */
|
|
25
|
+
export function colorizeDiff(theme: ThemeLike, diff: string): string {
|
|
26
|
+
return diff
|
|
27
|
+
.split("\n")
|
|
28
|
+
.map((line) => {
|
|
29
|
+
if (line.startsWith("+++") || line.startsWith("---")) return theme.fg("dim", line);
|
|
30
|
+
if (line.startsWith("@@")) return theme.fg("accent", line);
|
|
31
|
+
if (line.startsWith("+")) return theme.fg("success", line);
|
|
32
|
+
if (line.startsWith("-")) return theme.fg("error", line);
|
|
33
|
+
return line;
|
|
34
|
+
})
|
|
35
|
+
.join("\n");
|
|
36
|
+
}
|
package/src/summary.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { countLines, type ThemeLike } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure one-line summary builders for tool calls and results
|
|
5
|
+
* (ykn0309-style compact rendering). All return plain strings; the
|
|
6
|
+
* extension wraps them in pi-tui Text components.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function title(theme: ThemeLike, name: string): string {
|
|
10
|
+
return theme.fg("toolTitle", theme.bold(name)) + " ";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readCall(theme: ThemeLike, args: { path?: string; offset?: number; limit?: number }): string {
|
|
14
|
+
const range =
|
|
15
|
+
args.offset || args.limit
|
|
16
|
+
? theme.fg("dim", ` · lines ${args.offset ?? 1}${args.limit ? `–${(args.offset ?? 1) + args.limit - 1}` : "+"}`)
|
|
17
|
+
: "";
|
|
18
|
+
return title(theme, "Read") + theme.fg("accent", args.path ?? "") + range;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readSummary(theme: ThemeLike, output: string, truncated: boolean, failed: boolean): string {
|
|
22
|
+
if (failed) return theme.fg("error", output.split("\n")[0] || "Read failed");
|
|
23
|
+
const lines = countLines(output);
|
|
24
|
+
return theme.fg("dim", `${lines} ${lines === 1 ? "line" : "lines"}${truncated ? " · truncated" : ""}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function bashCall(theme: ThemeLike, command: string, expanded: boolean): string {
|
|
28
|
+
const lines = command.split(/\r\n|\r|\n/);
|
|
29
|
+
const head = lines[0] ?? "";
|
|
30
|
+
const omitted = lines.length - 1;
|
|
31
|
+
const shown = expanded || omitted === 0 ? command : `${head} ${theme.fg("dim", `… (+${omitted} ${omitted === 1 ? "line" : "lines"})`)}`;
|
|
32
|
+
return title(theme, "Bash") + theme.fg("accent", shown);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function bashSummary(theme: ThemeLike, output: string, failed: boolean): string {
|
|
36
|
+
if (failed) {
|
|
37
|
+
const first = output.split("\n").find((l) => l.trim()) ?? "Command failed";
|
|
38
|
+
return theme.fg("error", `✗ ${first}`);
|
|
39
|
+
}
|
|
40
|
+
const lines = countLines(output);
|
|
41
|
+
return theme.fg("success", "✓") + theme.fg("dim", lines > 0 ? ` ${lines} output ${lines === 1 ? "line" : "lines"}` : " done");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function editCall(theme: ThemeLike, args: { path?: string }): string {
|
|
45
|
+
return title(theme, "Edit") + theme.fg("accent", args.path ?? "");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function writeCall(theme: ThemeLike, args: { path?: string; content?: string }): string {
|
|
49
|
+
const lines = typeof args.content === "string" ? args.content.split("\n").length : 0;
|
|
50
|
+
return title(theme, "Write") + theme.fg("accent", args.path ?? "") + theme.fg("dim", ` · ${lines} ${lines === 1 ? "line" : "lines"}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function searchCall(
|
|
54
|
+
theme: ThemeLike,
|
|
55
|
+
name: string,
|
|
56
|
+
args: { pattern?: string; path?: string; glob?: string },
|
|
57
|
+
): string {
|
|
58
|
+
const pattern = args.pattern ?? args.glob ?? "";
|
|
59
|
+
const where = args.path ? theme.fg("dim", ` in ${args.path}`) : "";
|
|
60
|
+
return title(theme, name) + theme.fg("accent", pattern) + where;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function listCall(theme: ThemeLike, args: { path?: string }): string {
|
|
64
|
+
return title(theme, "List") + theme.fg("accent", args.path ?? ".");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function matchSummary(
|
|
68
|
+
theme: ThemeLike,
|
|
69
|
+
output: string,
|
|
70
|
+
failed: boolean,
|
|
71
|
+
noun: { one: string; many: string },
|
|
72
|
+
): string {
|
|
73
|
+
if (failed) return theme.fg("error", output.split("\n")[0] || "failed");
|
|
74
|
+
const lines = countLines(output);
|
|
75
|
+
if (lines === 0) return theme.fg("dim", `no ${noun.many}`);
|
|
76
|
+
return theme.fg("dim", `${lines} ${lines === 1 ? noun.one : noun.many}`);
|
|
77
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local structural types for @pify/pretty.
|
|
3
|
+
* No imports from pi packages: src/ typechecks and runs standalone.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Painter surface the summary builders need (subset of pi-tui Theme). */
|
|
7
|
+
export interface ThemeLike {
|
|
8
|
+
fg(color: string, text: string): string;
|
|
9
|
+
bold(text: string): string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The tools this extension can re-render. */
|
|
13
|
+
export const PRETTY_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"] as const;
|
|
14
|
+
export type PrettyTool = (typeof PRETTY_TOOLS)[number];
|
|
15
|
+
|
|
16
|
+
export interface PrettyConfig {
|
|
17
|
+
/** Renderers the user turned off (defaults: all on). */
|
|
18
|
+
disabled: PrettyTool[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_CONFIG: PrettyConfig = { disabled: [] };
|
|
22
|
+
|
|
23
|
+
export interface BranchEntryLike {
|
|
24
|
+
type?: string;
|
|
25
|
+
customType?: string;
|
|
26
|
+
data?: unknown;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
31
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Extract joined text from a tool result's content blocks. */
|
|
35
|
+
export function textContent(result: unknown): string {
|
|
36
|
+
if (!isRecord(result) || !Array.isArray(result.content)) return "";
|
|
37
|
+
return result.content
|
|
38
|
+
.filter((c): c is { type: string; text: string } => isRecord(c) && c.type === "text" && typeof c.text === "string")
|
|
39
|
+
.map((c) => c.text)
|
|
40
|
+
.join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function countLines(text: string): number {
|
|
44
|
+
if (!text) return 0;
|
|
45
|
+
return text.split("\n").filter((l) => l.trim() !== "").length;
|
|
46
|
+
}
|