pi-compact-transcript 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 +53 -0
- package/extensions/compact-transcript.ts +286 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alan Hagedorn
|
|
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,53 @@
|
|
|
1
|
+
# pi-compact-transcript
|
|
2
|
+
|
|
3
|
+
A compact transcript extension for [pi](https://pi.dev):
|
|
4
|
+
|
|
5
|
+
- Collapses hidden thinking blocks into `Thinking...` and consecutive thinking blocks into `Thinking... (Nx)`.
|
|
6
|
+
- Collapses built-in tool calls/results into one-line previews.
|
|
7
|
+
- Consecutive tool uses are summarized as `Used N tools <latest tool preview>`.
|
|
8
|
+
- Minimizes vertical space so long agent runs do not scroll away as quickly.
|
|
9
|
+
|
|
10
|
+
## Install from GitHub
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pi install git:github.com/avhagedorn/pi-compact-transcript
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or try it for one run:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pi -e git:github.com/avhagedorn/pi-compact-transcript
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Reload or restart pi after installing:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
/reload
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Install from npm
|
|
29
|
+
|
|
30
|
+
Once published to npm:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pi install npm:pi-compact-transcript
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Recommended settings
|
|
37
|
+
|
|
38
|
+
The extension works best with hidden thinking and no output padding:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"hideThinkingBlock": true,
|
|
43
|
+
"outputPad": 0
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Set these in `~/.pi/agent/settings.json`, or use `/settings` in pi.
|
|
48
|
+
|
|
49
|
+
## Notes
|
|
50
|
+
|
|
51
|
+
This extension overrides the built-in tool renderers for `bash`, `read`, `write`, `edit`, `grep`, `find`, and `ls`. It delegates execution to pi's original built-in tools; only display is changed.
|
|
52
|
+
|
|
53
|
+
The consecutive-thinking collapse uses pi's current internal assistant-message renderer. If pi changes that internal path in a future release, the extension falls back to the normal hidden-thinking label behavior.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
createBashTool,
|
|
4
|
+
createEditTool,
|
|
5
|
+
createFindTool,
|
|
6
|
+
createGrepTool,
|
|
7
|
+
createLsTool,
|
|
8
|
+
createReadTool,
|
|
9
|
+
createWriteTool,
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
12
|
+
import { createRequire } from "node:module";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
|
|
17
|
+
type BuiltInName = "bash" | "read" | "write" | "edit" | "grep" | "find" | "ls";
|
|
18
|
+
|
|
19
|
+
type ToolInfo = {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
args: any;
|
|
23
|
+
preview: string;
|
|
24
|
+
result?: string;
|
|
25
|
+
invalidate?: () => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const BUILT_INS: BuiltInName[] = ["bash", "read", "write", "edit", "grep", "find", "ls"];
|
|
29
|
+
const LABEL = "Thinking...";
|
|
30
|
+
|
|
31
|
+
const toolCache = new Map<string, ReturnType<typeof createBuiltInTools>>();
|
|
32
|
+
|
|
33
|
+
function createBuiltInTools(cwd: string) {
|
|
34
|
+
return {
|
|
35
|
+
bash: createBashTool(cwd),
|
|
36
|
+
read: createReadTool(cwd),
|
|
37
|
+
write: createWriteTool(cwd),
|
|
38
|
+
edit: createEditTool(cwd),
|
|
39
|
+
grep: createGrepTool(cwd),
|
|
40
|
+
find: createFindTool(cwd),
|
|
41
|
+
ls: createLsTool(cwd),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getBuiltInTools(cwd: string) {
|
|
46
|
+
let tools = toolCache.get(cwd);
|
|
47
|
+
if (!tools) {
|
|
48
|
+
tools = createBuiltInTools(cwd);
|
|
49
|
+
toolCache.set(cwd, tools);
|
|
50
|
+
}
|
|
51
|
+
return tools;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function shortenPath(path: unknown): string {
|
|
55
|
+
if (typeof path !== "string" || !path) return "";
|
|
56
|
+
const home = homedir();
|
|
57
|
+
return path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function oneLine(value: unknown): string {
|
|
61
|
+
return String(value ?? "")
|
|
62
|
+
.replace(/\s+/g, " ")
|
|
63
|
+
.trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function limitPlain(text: string, max = Math.max(20, (process.stdout.columns || 100) - 6)): string {
|
|
67
|
+
const clean = oneLine(text);
|
|
68
|
+
if (clean.length <= max) return clean;
|
|
69
|
+
return `${clean.slice(0, Math.max(0, max - 1))}…`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function quote(s: string): string {
|
|
73
|
+
return JSON.stringify(s);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function previewFor(name: string, args: any): string {
|
|
77
|
+
switch (name) {
|
|
78
|
+
case "bash":
|
|
79
|
+
return `$ ${oneLine(args?.command || "...")}`;
|
|
80
|
+
case "read": {
|
|
81
|
+
let out = `read ${shortenPath(args?.path) || "..."}`;
|
|
82
|
+
if (args?.offset !== undefined || args?.limit !== undefined) {
|
|
83
|
+
const start = args.offset ?? 1;
|
|
84
|
+
const end = args.limit !== undefined ? start + args.limit - 1 : "";
|
|
85
|
+
out += `:${start}${end ? `-${end}` : ""}`;
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
case "write": {
|
|
90
|
+
const lines = typeof args?.content === "string" ? args.content.split("\n").length : 0;
|
|
91
|
+
return `write ${shortenPath(args?.path) || "..."}${lines ? ` (${lines} lines)` : ""}`;
|
|
92
|
+
}
|
|
93
|
+
case "edit": {
|
|
94
|
+
const edits = Array.isArray(args?.edits) ? args.edits.length : 0;
|
|
95
|
+
return `edit ${shortenPath(args?.path) || "..."}${edits > 1 ? ` (${edits} edits)` : ""}`;
|
|
96
|
+
}
|
|
97
|
+
case "grep": {
|
|
98
|
+
const pattern = args?.pattern ? quote(String(args.pattern)) : "...";
|
|
99
|
+
const path = shortenPath(args?.path) || ".";
|
|
100
|
+
return `grep ${pattern} ${path}${args?.glob ? ` (${args.glob})` : ""}`;
|
|
101
|
+
}
|
|
102
|
+
case "find":
|
|
103
|
+
return `find ${args?.pattern ? quote(String(args.pattern)) : "..."} ${shortenPath(args?.path) || "."}`;
|
|
104
|
+
case "ls":
|
|
105
|
+
return `ls ${shortenPath(args?.path) || "."}`;
|
|
106
|
+
default:
|
|
107
|
+
return `${name} ${oneLine(JSON.stringify(args ?? {}))}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resultPreview(result: any): string {
|
|
112
|
+
const text = Array.isArray(result?.content)
|
|
113
|
+
? result.content.find((c: any) => c?.type === "text" && typeof c.text === "string")?.text
|
|
114
|
+
: undefined;
|
|
115
|
+
if (!text) return "";
|
|
116
|
+
const lines = String(text).trim().split("\n").filter(Boolean);
|
|
117
|
+
if (lines.length === 0) return "";
|
|
118
|
+
if (lines.length === 1) return lines[0];
|
|
119
|
+
return `${lines.length} lines`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function patchAssistantThinkingRenderer() {
|
|
123
|
+
try {
|
|
124
|
+
const require = createRequire(import.meta.url);
|
|
125
|
+
const piMain = require.resolve("@earendil-works/pi-coding-agent");
|
|
126
|
+
const distDir = dirname(piMain);
|
|
127
|
+
const assistantModule = await import(
|
|
128
|
+
pathToFileURL(join(distDir, "modes/interactive/components/assistant-message.js")).href
|
|
129
|
+
);
|
|
130
|
+
const themeModule = await import(pathToFileURL(join(distDir, "modes/interactive/theme/theme.js")).href);
|
|
131
|
+
const Component = assistantModule.AssistantMessageComponent;
|
|
132
|
+
if (!Component?.prototype || Component.prototype.__compactTranscriptPatched) return;
|
|
133
|
+
const original = Component.prototype.updateContent;
|
|
134
|
+
|
|
135
|
+
Component.prototype.updateContent = function (message: any) {
|
|
136
|
+
if (!this.hideThinkingBlock) return original.call(this, message);
|
|
137
|
+
|
|
138
|
+
this.lastMessage = message;
|
|
139
|
+
this.contentContainer.clear();
|
|
140
|
+
|
|
141
|
+
const visible = message.content.filter(
|
|
142
|
+
(c: any) => (c.type === "text" && c.text?.trim()) || (c.type === "thinking" && c.thinking?.trim()),
|
|
143
|
+
);
|
|
144
|
+
if (visible.length) this.contentContainer.addChild(new Spacer(1));
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < message.content.length; i++) {
|
|
147
|
+
const content = message.content[i];
|
|
148
|
+
if (content.type === "text" && content.text?.trim()) {
|
|
149
|
+
this.contentContainer.addChild(new Markdown(content.text.trim(), this.outputPad, 0, this.markdownTheme));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (content.type !== "thinking" || !content.thinking?.trim()) continue;
|
|
153
|
+
|
|
154
|
+
let count = 1;
|
|
155
|
+
while (
|
|
156
|
+
message.content[i + count]?.type === "thinking" &&
|
|
157
|
+
message.content[i + count]?.thinking?.trim()
|
|
158
|
+
) {
|
|
159
|
+
count++;
|
|
160
|
+
}
|
|
161
|
+
i += count - 1;
|
|
162
|
+
|
|
163
|
+
const label = count > 1 ? `${LABEL} (${count}x)` : LABEL;
|
|
164
|
+
this.contentContainer.addChild(
|
|
165
|
+
new Text(themeModule.theme.italic(themeModule.theme.fg("thinkingText", label)), this.outputPad, 0),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const hasVisibleAfter = message.content
|
|
169
|
+
.slice(i + 1)
|
|
170
|
+
.some((c: any) => (c.type === "text" && c.text?.trim()) || (c.type === "thinking" && c.thinking?.trim()));
|
|
171
|
+
if (hasVisibleAfter) this.contentContainer.addChild(new Spacer(1));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.hasToolCalls = message.content.some((c: any) => c.type === "toolCall");
|
|
175
|
+
};
|
|
176
|
+
Component.prototype.__compactTranscriptPatched = true;
|
|
177
|
+
} catch {
|
|
178
|
+
// Best-effort: older/bundled pi builds may not expose internal component modules.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export default async function (pi: ExtensionAPI) {
|
|
183
|
+
await patchAssistantThinkingRenderer();
|
|
184
|
+
let current: ToolInfo[] = [];
|
|
185
|
+
let byId = new Map<string, ToolInfo>();
|
|
186
|
+
let consecutiveThinking = 0;
|
|
187
|
+
|
|
188
|
+
function thinkingLabel() {
|
|
189
|
+
return consecutiveThinking > 1 ? `${LABEL} (${consecutiveThinking}x)` : LABEL;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function resetTools() {
|
|
193
|
+
current = [];
|
|
194
|
+
byId = new Map();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function applyThinkingLabel(ctx: ExtensionContext) {
|
|
198
|
+
ctx.ui.setHiddenThinkingLabel(thinkingLabel());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
202
|
+
applyThinkingLabel(ctx);
|
|
203
|
+
ctx.ui.setWorkingMessage(LABEL);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
207
|
+
resetTools();
|
|
208
|
+
consecutiveThinking = 0;
|
|
209
|
+
applyThinkingLabel(ctx);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
pi.on("message_update", (event: any, ctx) => {
|
|
213
|
+
const type = event.assistantMessageEvent?.type;
|
|
214
|
+
if (type === "thinking_start") {
|
|
215
|
+
consecutiveThinking++;
|
|
216
|
+
applyThinkingLabel(ctx);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (type && !type.startsWith("thinking_")) {
|
|
220
|
+
consecutiveThinking = 0;
|
|
221
|
+
applyThinkingLabel(ctx);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
pi.on("tool_execution_start", (event: any) => {
|
|
226
|
+
const previous = current[current.length - 1];
|
|
227
|
+
const info: ToolInfo = {
|
|
228
|
+
id: event.toolCallId,
|
|
229
|
+
name: event.toolName,
|
|
230
|
+
args: event.args,
|
|
231
|
+
preview: previewFor(event.toolName, event.args),
|
|
232
|
+
};
|
|
233
|
+
current.push(info);
|
|
234
|
+
byId.set(info.id, info);
|
|
235
|
+
previous?.invalidate?.();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
pi.on("tool_execution_end", (event: any) => {
|
|
239
|
+
const info = byId.get(event.toolCallId);
|
|
240
|
+
if (!info) return;
|
|
241
|
+
const suffix = resultPreview(event.result);
|
|
242
|
+
if (suffix) info.result = suffix;
|
|
243
|
+
info.invalidate?.();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
function compactLine(toolCallId: string, name: string, args: any, theme: any, context: any): string {
|
|
247
|
+
let info = byId.get(toolCallId);
|
|
248
|
+
if (!info) {
|
|
249
|
+
info = { id: toolCallId, name, args, preview: previewFor(name, args) };
|
|
250
|
+
byId.set(toolCallId, info);
|
|
251
|
+
if (!current.some((t) => t.id === toolCallId)) current.push(info);
|
|
252
|
+
}
|
|
253
|
+
info.invalidate = context.invalidate;
|
|
254
|
+
info.args = args;
|
|
255
|
+
info.preview = previewFor(name, args);
|
|
256
|
+
|
|
257
|
+
if (current[current.length - 1]?.id !== toolCallId) return "";
|
|
258
|
+
|
|
259
|
+
const details = info.result ? `${info.preview} {${oneLine(info.result)}}` : info.preview;
|
|
260
|
+
if (current.length === 1) return theme.fg("muted", limitPlain(details));
|
|
261
|
+
const prefix = `Used ${current.length} tools `;
|
|
262
|
+
return theme.fg("toolTitle", prefix) + theme.fg("muted", limitPlain(details, Math.max(20, (process.stdout.columns || 100) - prefix.length - 6)));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
for (const name of BUILT_INS) {
|
|
266
|
+
const base = getBuiltInTools(process.cwd())[name] as any;
|
|
267
|
+
pi.registerTool({
|
|
268
|
+
...base,
|
|
269
|
+
name,
|
|
270
|
+
label: name,
|
|
271
|
+
renderShell: "self",
|
|
272
|
+
async execute(toolCallId: string, params: any, signal: AbortSignal | undefined, onUpdate: any, ctx: ExtensionContext) {
|
|
273
|
+
const tool = (getBuiltInTools(ctx.cwd) as any)[name];
|
|
274
|
+
return tool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
275
|
+
},
|
|
276
|
+
renderCall(args: any, theme: any, context: any) {
|
|
277
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
278
|
+
text.setText(compactLine(context.toolCallId, name, args, theme, context));
|
|
279
|
+
return text;
|
|
280
|
+
},
|
|
281
|
+
renderResult(_result: any, _options: any, _theme: any, _context: any) {
|
|
282
|
+
return new Text("", 0, 0);
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-compact-transcript",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A compact transcript extension for pi: collapsed thinking and one-line tool previews.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Alan Hagedorn <avhagedorn@gmail.com>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/avhagedorn/pi-compact-transcript.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/avhagedorn/pi-compact-transcript/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/avhagedorn/pi-compact-transcript#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pi-package",
|
|
18
|
+
"pi-coding-agent",
|
|
19
|
+
"pi-extension",
|
|
20
|
+
"transcript",
|
|
21
|
+
"terminal"
|
|
22
|
+
],
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": ["./extensions/compact-transcript.ts"]
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
28
|
+
"@earendil-works/pi-tui": "*"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"extensions",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
}
|
|
38
|
+
}
|