pi-pretty-tui 0.1.2 → 0.1.4
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 -4
- package/assets/screenshot.png +0 -0
- package/extensions/index.ts +980 -30
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -7,8 +7,11 @@ A visual refinement extension for the [Pi coding agent](https://pi.dev/). It pro
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- Compact `Read`, `Bash`, `Edit`, `Write`, `Grep`, `Find`, and `List` calls
|
|
10
|
-
-
|
|
11
|
-
-
|
|
10
|
+
- Concise per-tool call and result summaries with expandable details
|
|
11
|
+
- Native-style `Write` previews in `full` mode: first 10 lines when collapsed, full content when expanded
|
|
12
|
+
- Live `Bash` output in `full` mode: latest 5 lines when collapsed, all available output when expanded
|
|
13
|
+
- Switchable `full`, `compact`, and `clean` rendering modes with persistent settings
|
|
14
|
+
- Clean mode hides thinking blocks (including Pi's standalone `Thinking...` placeholder) while collapsed and reveals the original thinking content when expanded
|
|
12
15
|
- Colored `+added` and `-removed` edit statistics and diffs
|
|
13
16
|
- Correct hanging indentation for long paths and wrapped tool output
|
|
14
17
|
- Bullet (`•`) markers for unordered Markdown lists
|
|
@@ -23,13 +26,32 @@ Restart Pi or run `/reload` after installation.
|
|
|
23
26
|
|
|
24
27
|
Pi may warn that built-in tools are being overridden. This is expected: pi-pretty-tui re-registers Pi's built-in tools and delegates execution to their original implementations, changing only their TUI renderers.
|
|
25
28
|
|
|
29
|
+
## Rendering modes
|
|
30
|
+
|
|
31
|
+
Run `/pretty-tui` to choose a mode interactively, or set one directly:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
/pretty-tui full
|
|
35
|
+
/pretty-tui compact
|
|
36
|
+
/pretty-tui clean
|
|
37
|
+
/pretty-tui status
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `full` (default): preserve each tool's detailed call renderer and normal result preview. Press `Ctrl+O` to expand available output, diffs, and content.
|
|
41
|
+
- `compact`: when collapsed, all supported built-in tools use concise one-line call summaries and final result summaries. Long commands, written content, edit diffs, and tool output stay hidden until expanded; press `Ctrl+O` to reveal the full details.
|
|
42
|
+
- `clean`: collapse supported tool calls into a single `Running(...)` status; the activity slot shows a tool name as soon as its streamed tool call appears, such as `Running(3 tool calls · Read)`, and remains visible for at least 1 second before changing to `thinking...`. Once settled, the label becomes `Done(...)`. Visible assistant text starts a new tool group, so later calls get a separate status. Collapsed thinking blocks stay hidden without Pi's `Thinking...` placeholder; press `Ctrl+O` to reveal thinking content and normal expanded tool calls/output in transcript order.
|
|
43
|
+
|
|
44
|
+
Clean mode covers the built-in tools managed by this package: `read`, `bash`, `edit`, `write`, `grep`, `find`, and `ls`. Third-party tools keep their own rendering.
|
|
45
|
+
|
|
46
|
+
The selected mode applies immediately and persists in `~/.pi/agent/pretty-tui.json` (or the directory selected by `PI_CODING_AGENT_DIR`).
|
|
47
|
+
|
|
26
48
|
## Expand tool output
|
|
27
49
|
|
|
28
|
-
Press `Ctrl+O` (Pi's default `app.tools.expand` keybinding) to show or hide detailed output and
|
|
50
|
+
Press `Ctrl+O` (Pi's default `app.tools.expand` keybinding) to show or hide detailed output, edit diffs, complete `Write` content, full tool details, and thinking content in `compact` or `clean` mode.
|
|
29
51
|
|
|
30
52
|
## Compatibility notice
|
|
31
53
|
|
|
32
|
-
Tool rendering uses Pi's documented extension APIs. Changing unordered-list markers currently requires runtime patching of an internal `@earendil-works/pi-tui` Markdown renderer method because Pi does not expose a public hook for that presentation detail. Code blocks use Pi's original renderer without modification.
|
|
54
|
+
Tool rendering uses Pi's documented extension APIs. Clean thinking and active-tool state follow Pi's built-in expansion and execution transitions through its exported interactive components. Changing unordered-list markers currently requires runtime patching of an internal `@earendil-works/pi-tui` Markdown renderer method because Pi does not expose a public hook for that presentation detail. Code blocks use Pi's original renderer without modification.
|
|
33
55
|
|
|
34
56
|
No Pi source files are modified. The list-marker patch is removed during session shutdown, but a future Pi release may require this extension to be updated.
|
|
35
57
|
|
package/assets/screenshot.png
CHANGED
|
Binary file
|
package/extensions/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import {
|
|
2
|
+
AssistantMessageComponent,
|
|
3
|
+
InteractiveMode,
|
|
4
|
+
ToolExecutionComponent,
|
|
3
5
|
createBashTool,
|
|
4
6
|
createEditTool,
|
|
5
7
|
createFindTool,
|
|
@@ -7,6 +9,8 @@ import {
|
|
|
7
9
|
createLsTool,
|
|
8
10
|
createReadTool,
|
|
9
11
|
createWriteTool,
|
|
12
|
+
getAgentDir,
|
|
13
|
+
type ExtensionAPI,
|
|
10
14
|
} from "@earendil-works/pi-coding-agent";
|
|
11
15
|
import {
|
|
12
16
|
type Component,
|
|
@@ -14,10 +18,208 @@ import {
|
|
|
14
18
|
visibleWidth,
|
|
15
19
|
wrapTextWithAnsi,
|
|
16
20
|
} from "@earendil-works/pi-tui";
|
|
21
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
|
|
24
|
+
type PrettyTuiMode = "full" | "compact" | "clean";
|
|
25
|
+
const CLEAN_TOOL_ACTIVITY_MIN_MS = 1000;
|
|
26
|
+
type PrettyTuiConfig = {
|
|
27
|
+
mode?: PrettyTuiMode;
|
|
28
|
+
/** Legacy location used by the first mode implementation. */
|
|
29
|
+
bash?: {
|
|
30
|
+
mode?: "full" | "compact";
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const loadConfig = (path: string): PrettyTuiConfig => {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
37
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
};
|
|
17
42
|
|
|
18
43
|
/** Polished tool calls and list rendering for Pi's TUI. */
|
|
19
44
|
export default function prettyTui(pi: ExtensionAPI) {
|
|
20
45
|
const cwd = process.cwd();
|
|
46
|
+
const configPath = join(getAgentDir(), "pretty-tui.json");
|
|
47
|
+
let config = loadConfig(configPath);
|
|
48
|
+
const configuredMode = config.mode ?? config.bash?.mode;
|
|
49
|
+
let renderMode: PrettyTuiMode = configuredMode === "compact" || configuredMode === "clean"
|
|
50
|
+
? configuredMode
|
|
51
|
+
: "full";
|
|
52
|
+
let cleanToolsExpanded = false;
|
|
53
|
+
|
|
54
|
+
const saveRenderMode = (mode: PrettyTuiMode) => {
|
|
55
|
+
config = { ...config, mode };
|
|
56
|
+
mkdirSync(getAgentDir(), { recursive: true });
|
|
57
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
pi.registerCommand("pretty-tui", {
|
|
61
|
+
description: "Configure pi-pretty-tui rendering mode",
|
|
62
|
+
getArgumentCompletions: (prefix: string) => {
|
|
63
|
+
const items = [
|
|
64
|
+
{ value: "full", label: "full", description: "Full tool details and output" },
|
|
65
|
+
{ value: "compact", label: "compact", description: "Concise summaries for all built-in tools" },
|
|
66
|
+
{ value: "clean", label: "clean", description: "Group supported tools into Running/Done status" },
|
|
67
|
+
{ value: "status", label: "status", description: "Show the current mode" },
|
|
68
|
+
];
|
|
69
|
+
const filtered = items.filter((item) => item.value.startsWith(prefix.trim().toLowerCase()));
|
|
70
|
+
return filtered.length > 0 ? filtered : null;
|
|
71
|
+
},
|
|
72
|
+
handler: async (args, ctx) => {
|
|
73
|
+
let requested = args.trim().toLowerCase();
|
|
74
|
+
|
|
75
|
+
if (!requested) {
|
|
76
|
+
if (!ctx.hasUI) {
|
|
77
|
+
ctx.ui.notify(`pi-pretty-tui mode: ${renderMode}`, "info");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const full = `Full — full tool details and output${renderMode === "full" ? " (current)" : ""}`;
|
|
81
|
+
const compact = `Compact — concise summaries for all built-in tools${renderMode === "compact" ? " (current)" : ""}`;
|
|
82
|
+
const clean = `Clean — group supported tools into Running/Done status${renderMode === "clean" ? " (current)" : ""}`;
|
|
83
|
+
const selected = await ctx.ui.select("pi-pretty-tui rendering mode", [full, compact, clean]);
|
|
84
|
+
if (!selected) return;
|
|
85
|
+
requested = selected === full ? "full" : selected === compact ? "compact" : "clean";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (requested === "status") {
|
|
89
|
+
ctx.ui.notify(`pi-pretty-tui mode: ${renderMode}`, "info");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (requested !== "full" && requested !== "compact" && requested !== "clean") {
|
|
93
|
+
ctx.ui.notify("Usage: /pretty-tui [full|compact|clean|status]", "error");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
renderMode = requested;
|
|
98
|
+
try {
|
|
99
|
+
saveRenderMode(renderMode);
|
|
100
|
+
ctx.ui.notify(`pi-pretty-tui mode set to: ${renderMode}`, "info");
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
103
|
+
ctx.ui.notify(`Mode changed for this session, but could not save settings: ${message}`, "warning");
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Pi's built-in assistant component turns hidden thinking into a static
|
|
109
|
+
// "Thinking..." label. In collapsed clean mode, omit that label entirely;
|
|
110
|
+
// the clean-mode Running(...) row owns activity, while Ctrl+O still reveals
|
|
111
|
+
// the thinking content. The component is exported by Pi specifically for
|
|
112
|
+
// extension-level rendering customizations, so patch its public methods
|
|
113
|
+
// rather than Pi's source.
|
|
114
|
+
const assistantPrototype = AssistantMessageComponent.prototype as any;
|
|
115
|
+
const thinkingPatchKey = Symbol.for("pretty-tui.clean-thinking");
|
|
116
|
+
if (!assistantPrototype[thinkingPatchKey]) {
|
|
117
|
+
const originalUpdateContent = assistantPrototype.updateContent;
|
|
118
|
+
const originalRender = assistantPrototype.render;
|
|
119
|
+
const originalMessageKey = Symbol("pretty-tui.original-assistant-message");
|
|
120
|
+
const renderedModeKey = Symbol("pretty-tui.rendered-assistant-mode");
|
|
121
|
+
const renderedExpansionKey = Symbol("pretty-tui.rendered-thinking-expansion");
|
|
122
|
+
|
|
123
|
+
const patchedUpdateContent = function (this: any, message: any, isStreaming = this.isStreaming) {
|
|
124
|
+
if (renderMode !== "clean") {
|
|
125
|
+
originalUpdateContent.call(this, message, isStreaming);
|
|
126
|
+
this[originalMessageKey] = message;
|
|
127
|
+
this[renderedModeKey] = renderMode;
|
|
128
|
+
this[renderedExpansionKey] = undefined;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const showThinking = cleanToolsExpanded;
|
|
133
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
134
|
+
// In collapsed clean mode, omit Pi's built-in Thinking... placeholder
|
|
135
|
+
// entirely. The clean-mode Running(...) row owns the activity label;
|
|
136
|
+
// expanded mode still reveals the actual thinking content.
|
|
137
|
+
const displayMessage = showThinking
|
|
138
|
+
? message
|
|
139
|
+
: { ...message, content: content.filter((item: any) => item.type !== "thinking") };
|
|
140
|
+
const previousHideThinkingBlock = this.hideThinkingBlock;
|
|
141
|
+
this.hideThinkingBlock = false;
|
|
142
|
+
try {
|
|
143
|
+
originalUpdateContent.call(this, displayMessage, isStreaming);
|
|
144
|
+
} finally {
|
|
145
|
+
this.hideThinkingBlock = previousHideThinkingBlock;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Pi's invalidate()/setHideThinkingBlock() call updateContent with
|
|
149
|
+
// lastMessage, so retain the unfiltered message for later mode changes.
|
|
150
|
+
this[originalMessageKey] = message;
|
|
151
|
+
this[renderedModeKey] = renderMode;
|
|
152
|
+
this[renderedExpansionKey] = showThinking;
|
|
153
|
+
this.lastMessage = message;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const patchedRender = function (this: any, width: number): string[] {
|
|
157
|
+
const needsRefresh = this[originalMessageKey] && (
|
|
158
|
+
this[renderedModeKey] !== renderMode ||
|
|
159
|
+
(renderMode === "clean" && this[renderedExpansionKey] !== cleanToolsExpanded)
|
|
160
|
+
);
|
|
161
|
+
if (needsRefresh) {
|
|
162
|
+
patchedUpdateContent.call(this, this[originalMessageKey], this.isStreaming);
|
|
163
|
+
}
|
|
164
|
+
return originalRender.call(this, width);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
assistantPrototype[thinkingPatchKey] = {
|
|
168
|
+
originalUpdateContent,
|
|
169
|
+
patchedUpdateContent,
|
|
170
|
+
originalRender,
|
|
171
|
+
patchedRender,
|
|
172
|
+
};
|
|
173
|
+
assistantPrototype.updateContent = patchedUpdateContent;
|
|
174
|
+
assistantPrototype.render = patchedRender;
|
|
175
|
+
|
|
176
|
+
pi.on("session_shutdown", () => {
|
|
177
|
+
const patch = assistantPrototype[thinkingPatchKey];
|
|
178
|
+
if (!patch) return;
|
|
179
|
+
if (patch.patchedUpdateContent === assistantPrototype.updateContent) {
|
|
180
|
+
assistantPrototype.updateContent = patch.originalUpdateContent;
|
|
181
|
+
}
|
|
182
|
+
if (patch.patchedRender === assistantPrototype.render) {
|
|
183
|
+
assistantPrototype.render = patch.originalRender;
|
|
184
|
+
}
|
|
185
|
+
if (
|
|
186
|
+
assistantPrototype.updateContent === patch.originalUpdateContent &&
|
|
187
|
+
assistantPrototype.render === patch.originalRender
|
|
188
|
+
) {
|
|
189
|
+
delete assistantPrototype[thinkingPatchKey];
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Extension shortcuts cannot replace Pi's built-in Ctrl+O binding. Wrap
|
|
195
|
+
// the exported state transition instead, so both Ctrl+O and UI callers keep
|
|
196
|
+
// their normal behavior while clean thinking follows the same state.
|
|
197
|
+
const interactiveModePrototype = InteractiveMode.prototype as any;
|
|
198
|
+
const toolsExpansionPatchKey = Symbol.for("pretty-tui.clean-tool-expansion");
|
|
199
|
+
if (!interactiveModePrototype[toolsExpansionPatchKey]) {
|
|
200
|
+
const originalSetToolsExpanded = interactiveModePrototype.setToolsExpanded;
|
|
201
|
+
const patchedSetToolsExpanded = function (this: any, expanded: boolean) {
|
|
202
|
+
cleanToolsExpanded = expanded;
|
|
203
|
+
return originalSetToolsExpanded.call(this, expanded);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
interactiveModePrototype[toolsExpansionPatchKey] = {
|
|
207
|
+
originalSetToolsExpanded,
|
|
208
|
+
patchedSetToolsExpanded,
|
|
209
|
+
};
|
|
210
|
+
interactiveModePrototype.setToolsExpanded = patchedSetToolsExpanded;
|
|
211
|
+
|
|
212
|
+
pi.on("session_shutdown", () => {
|
|
213
|
+
const patch = interactiveModePrototype[toolsExpansionPatchKey];
|
|
214
|
+
if (!patch) return;
|
|
215
|
+
if (patch.patchedSetToolsExpanded === interactiveModePrototype.setToolsExpanded) {
|
|
216
|
+
interactiveModePrototype.setToolsExpanded = patch.originalSetToolsExpanded;
|
|
217
|
+
}
|
|
218
|
+
if (interactiveModePrototype.setToolsExpanded === patch.originalSetToolsExpanded) {
|
|
219
|
+
delete interactiveModePrototype[toolsExpansionPatchKey];
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
21
223
|
|
|
22
224
|
// Pi normalizes unordered-list markers to "-". Replace only that marker;
|
|
23
225
|
// leave code-block rendering entirely to Pi's built-in Markdown renderer.
|
|
@@ -101,20 +303,53 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
101
303
|
context.state.compactToolStatus = status;
|
|
102
304
|
};
|
|
103
305
|
|
|
306
|
+
const callRow = (theme: any, name: string, detail: string, state: any): DisplayRow => ({
|
|
307
|
+
prefix: () => {
|
|
308
|
+
const status = (state.compactToolStatus ?? "running") as ToolStatus;
|
|
309
|
+
const color = status === "success" ? "success" : status === "error" ? "error" : "dim";
|
|
310
|
+
return theme.fg(color, "● ");
|
|
311
|
+
},
|
|
312
|
+
continuation: " ",
|
|
313
|
+
content:
|
|
314
|
+
theme.fg("accent", theme.bold(name)) +
|
|
315
|
+
theme.fg("dim", "(") +
|
|
316
|
+
theme.fg("text", detail) +
|
|
317
|
+
theme.fg("dim", ")"),
|
|
318
|
+
});
|
|
319
|
+
|
|
104
320
|
const call = (theme: any, name: string, detail: string, state: any) =>
|
|
105
|
-
block([
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
321
|
+
block([callRow(theme, name, detail, state)]);
|
|
322
|
+
|
|
323
|
+
const writeCall = (theme: any, path: string, content: string, expanded: boolean, state: any) => {
|
|
324
|
+
const lines = content.replace(/\t/g, " ").split("\n");
|
|
325
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
326
|
+
|
|
327
|
+
const total = lines.length;
|
|
328
|
+
const shown = lines.slice(0, expanded ? total : 10);
|
|
329
|
+
const remaining = total - shown.length;
|
|
330
|
+
const rows: DisplayRow[] = [
|
|
331
|
+
callRow(theme, "Write", `${path} · ${total} ${total === 1 ? "line" : "lines"}`, state),
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
for (let index = 0; index < shown.length; index++) {
|
|
335
|
+
const isLast = index === shown.length - 1 && remaining === 0;
|
|
336
|
+
rows.push({
|
|
337
|
+
prefix: theme.fg("dim", isLast ? " └ " : " │ "),
|
|
338
|
+
continuation: theme.fg("dim", " │ "),
|
|
339
|
+
content: theme.fg("toolOutput", shown[index] || " "),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (remaining > 0) {
|
|
344
|
+
rows.push({
|
|
345
|
+
prefix: theme.fg("muted", " └ "),
|
|
346
|
+
continuation: " ",
|
|
347
|
+
content: theme.fg("muted", `… ${remaining} more ${remaining === 1 ? "line" : "lines"}`),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return block(rows);
|
|
352
|
+
};
|
|
118
353
|
|
|
119
354
|
const result = (
|
|
120
355
|
theme: any,
|
|
@@ -161,25 +396,695 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
161
396
|
return result(theme, label);
|
|
162
397
|
};
|
|
163
398
|
|
|
399
|
+
const terminalOutputLines = (output: string): string[] => {
|
|
400
|
+
const lines: string[] = [];
|
|
401
|
+
let current = "";
|
|
402
|
+
|
|
403
|
+
for (let index = 0; index < output.length; index++) {
|
|
404
|
+
const char = output[index];
|
|
405
|
+
if (char === "\r") {
|
|
406
|
+
if (output[index + 1] === "\n") {
|
|
407
|
+
lines.push(current);
|
|
408
|
+
current = "";
|
|
409
|
+
index++;
|
|
410
|
+
} else {
|
|
411
|
+
// A bare carriage return redraws the current terminal line. Progress
|
|
412
|
+
// bars such as tqdm use this to update in place.
|
|
413
|
+
current = "";
|
|
414
|
+
}
|
|
415
|
+
} else if (char === "\n") {
|
|
416
|
+
lines.push(current);
|
|
417
|
+
current = "";
|
|
418
|
+
} else {
|
|
419
|
+
current += char;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (current) lines.push(current);
|
|
424
|
+
return lines;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const bashResult = (
|
|
428
|
+
context: any,
|
|
429
|
+
theme: any,
|
|
430
|
+
summary: string,
|
|
431
|
+
output: string,
|
|
432
|
+
expanded: boolean,
|
|
433
|
+
status: ToolStatus,
|
|
434
|
+
): Component => {
|
|
435
|
+
setStatus(context, status);
|
|
436
|
+
const lines = terminalOutputLines(output);
|
|
437
|
+
const shown = expanded ? lines : lines.slice(-5);
|
|
438
|
+
const omitted = lines.length - shown.length;
|
|
439
|
+
const rows: DisplayRow[] = [{
|
|
440
|
+
prefix: theme.fg("dim", "└ "),
|
|
441
|
+
continuation: " ",
|
|
442
|
+
content: theme.fg(status === "error" ? "error" : "toolOutput", summary),
|
|
443
|
+
}];
|
|
444
|
+
|
|
445
|
+
if (omitted > 0) {
|
|
446
|
+
rows.push({
|
|
447
|
+
prefix: theme.fg("muted", " │ "),
|
|
448
|
+
continuation: theme.fg("muted", " │ "),
|
|
449
|
+
content: theme.fg("muted", `… ${omitted} earlier ${omitted === 1 ? "line" : "lines"}`),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
for (let index = 0; index < shown.length; index++) {
|
|
454
|
+
const isLast = index === shown.length - 1;
|
|
455
|
+
rows.push({
|
|
456
|
+
prefix: theme.fg("dim", isLast ? " └ " : " │ "),
|
|
457
|
+
continuation: theme.fg("dim", " │ "),
|
|
458
|
+
content: theme.fg("toolOutput", shown[index] || " "),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return block(rows);
|
|
463
|
+
};
|
|
464
|
+
|
|
164
465
|
const completeStatus = (context: any, output: string): boolean => {
|
|
165
466
|
const failed = isError(context, output);
|
|
166
467
|
setStatus(context, failed ? "error" : "success");
|
|
167
468
|
return failed;
|
|
168
469
|
};
|
|
169
470
|
|
|
471
|
+
type ToolSummaryGroup = {
|
|
472
|
+
count: number;
|
|
473
|
+
failed: number;
|
|
474
|
+
lastToolCallId: string;
|
|
475
|
+
activity?: string;
|
|
476
|
+
};
|
|
477
|
+
type ToolSummaryData = {
|
|
478
|
+
count?: number;
|
|
479
|
+
failed?: number;
|
|
480
|
+
/** Identifies the last tool component for older single-group entries. */
|
|
481
|
+
lastToolCallId?: string;
|
|
482
|
+
/** All groups from a run, used to restore summaries after reload. */
|
|
483
|
+
groups?: ToolSummaryGroup[];
|
|
484
|
+
};
|
|
485
|
+
const supportedTools = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
|
|
486
|
+
type ToolActivityHold = {
|
|
487
|
+
toolCallId: string;
|
|
488
|
+
name: string;
|
|
489
|
+
until: number;
|
|
490
|
+
after: string;
|
|
491
|
+
started: boolean;
|
|
492
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
493
|
+
};
|
|
494
|
+
const settledSummaries = new Map<string, { count: number; failed: number; activity: DisplayValue }>();
|
|
495
|
+
const toolActivityHolds = new Map<string, ToolActivityHold>();
|
|
496
|
+
const legacySummaryLastToolCallIds = new Map<string, string>();
|
|
497
|
+
const knownToolCallIds = new Set<string>();
|
|
498
|
+
type CleanRunState = {
|
|
499
|
+
activeToolCallId?: string;
|
|
500
|
+
lastCompletedToolCallId?: string;
|
|
501
|
+
activeToolName?: string;
|
|
502
|
+
activity?: string;
|
|
503
|
+
requestRender?: () => void;
|
|
504
|
+
count: number;
|
|
505
|
+
failed: number;
|
|
506
|
+
groups: ToolSummaryGroup[];
|
|
507
|
+
active: boolean;
|
|
508
|
+
settled: boolean;
|
|
509
|
+
};
|
|
510
|
+
const cleanRun: CleanRunState = {
|
|
511
|
+
count: 0,
|
|
512
|
+
failed: 0,
|
|
513
|
+
groups: [],
|
|
514
|
+
active: false,
|
|
515
|
+
settled: false,
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const toolDisplayName = (name: string): string =>
|
|
519
|
+
name === "ls" ? "List" : name.charAt(0).toUpperCase() + name.slice(1);
|
|
520
|
+
|
|
521
|
+
const clearToolActivityHolds = () => {
|
|
522
|
+
for (const hold of toolActivityHolds.values()) {
|
|
523
|
+
if (hold.timer) clearTimeout(hold.timer);
|
|
524
|
+
}
|
|
525
|
+
toolActivityHolds.clear();
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const heldActivity = (hold: ToolActivityHold): string =>
|
|
529
|
+
Date.now() < hold.until ? hold.name : hold.after;
|
|
530
|
+
|
|
531
|
+
const scheduleToolActivityRelease = (hold: ToolActivityHold) => {
|
|
532
|
+
if (hold.timer) clearTimeout(hold.timer);
|
|
533
|
+
const delay = Math.max(0, hold.until - Date.now());
|
|
534
|
+
hold.timer = setTimeout(() => {
|
|
535
|
+
hold.timer = undefined;
|
|
536
|
+
if (Date.now() < hold.until) {
|
|
537
|
+
scheduleToolActivityRelease(hold);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (toolActivityHolds.get(hold.toolCallId) === hold) {
|
|
541
|
+
toolActivityHolds.delete(hold.toolCallId);
|
|
542
|
+
}
|
|
543
|
+
if (cleanRun.lastCompletedToolCallId === hold.toolCallId && !cleanRun.activeToolCallId) {
|
|
544
|
+
cleanRun.activeToolName = undefined;
|
|
545
|
+
cleanRun.activity = hold.after;
|
|
546
|
+
}
|
|
547
|
+
cleanRun.requestRender?.();
|
|
548
|
+
}, delay);
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const beginToolActivity = (toolCallId: string, name: string, started = false) => {
|
|
552
|
+
const previous = toolActivityHolds.get(toolCallId);
|
|
553
|
+
if (previous) {
|
|
554
|
+
if (previous.timer) clearTimeout(previous.timer);
|
|
555
|
+
previous.name = name;
|
|
556
|
+
previous.started ||= started;
|
|
557
|
+
cleanRun.activeToolName = name;
|
|
558
|
+
cleanRun.activity = name;
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const startedAt = Date.now();
|
|
562
|
+
toolActivityHolds.set(toolCallId, {
|
|
563
|
+
toolCallId,
|
|
564
|
+
name,
|
|
565
|
+
until: startedAt + CLEAN_TOOL_ACTIVITY_MIN_MS,
|
|
566
|
+
after: "thinking...",
|
|
567
|
+
started,
|
|
568
|
+
});
|
|
569
|
+
cleanRun.activeToolName = name;
|
|
570
|
+
cleanRun.activity = name;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const holdToolActivity = (toolCallId: string, name: string) => {
|
|
574
|
+
let hold = toolActivityHolds.get(toolCallId);
|
|
575
|
+
if (!hold) {
|
|
576
|
+
const now = Date.now();
|
|
577
|
+
hold = {
|
|
578
|
+
toolCallId,
|
|
579
|
+
name,
|
|
580
|
+
until: now + CLEAN_TOOL_ACTIVITY_MIN_MS,
|
|
581
|
+
after: "thinking...",
|
|
582
|
+
started: true,
|
|
583
|
+
};
|
|
584
|
+
toolActivityHolds.set(toolCallId, hold);
|
|
585
|
+
}
|
|
586
|
+
hold.name = name;
|
|
587
|
+
hold.started = true;
|
|
588
|
+
hold.after = "thinking...";
|
|
589
|
+
scheduleToolActivityRelease(hold);
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const activityValueForHold = (toolCallId: string, after: string): DisplayValue => {
|
|
593
|
+
const hold = toolActivityHolds.get(toolCallId);
|
|
594
|
+
if (!hold) return after;
|
|
595
|
+
hold.after = after;
|
|
596
|
+
return () => heldActivity(hold);
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const currentCleanActivity = (): string => {
|
|
600
|
+
if (cleanRun.activeToolCallId) {
|
|
601
|
+
return cleanRun.activeToolName ?? cleanRun.activity ?? "thinking...";
|
|
602
|
+
}
|
|
603
|
+
if (cleanRun.lastCompletedToolCallId) {
|
|
604
|
+
const hold = toolActivityHolds.get(cleanRun.lastCompletedToolCallId);
|
|
605
|
+
if (hold) return heldActivity(hold);
|
|
606
|
+
}
|
|
607
|
+
return cleanRun.activeToolName ?? cleanRun.activity ?? (cleanRun.active ? "thinking..." : "done");
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
pi.on("session_shutdown", clearToolActivityHolds);
|
|
611
|
+
|
|
612
|
+
// A tool component can be created from the streamed assistant message
|
|
613
|
+
// before Pi dispatches tool_execution_start. Mark its activity at the
|
|
614
|
+
// component's own execution boundary too, so the first clean render names it.
|
|
615
|
+
const toolExecutionPrototype = ToolExecutionComponent.prototype as any;
|
|
616
|
+
const toolExecutionPatchKey = Symbol.for("pretty-tui.clean-tool-execution");
|
|
617
|
+
if (!toolExecutionPrototype[toolExecutionPatchKey]) {
|
|
618
|
+
const originalMarkExecutionStarted = toolExecutionPrototype.markExecutionStarted;
|
|
619
|
+
const originalToolRender = toolExecutionPrototype.render;
|
|
620
|
+
const renderedModeKey = Symbol("pretty-tui.tool-rendered-mode");
|
|
621
|
+
const patchedMarkExecutionStarted = function (this: any) {
|
|
622
|
+
if (supportedTools.has(this.toolName)) {
|
|
623
|
+
if (typeof this.ui?.requestRender === "function") {
|
|
624
|
+
cleanRun.requestRender = () => this.ui.requestRender();
|
|
625
|
+
}
|
|
626
|
+
cleanRun.active = true;
|
|
627
|
+
cleanRun.activeToolCallId = this.toolCallId;
|
|
628
|
+
beginToolActivity(this.toolCallId, toolDisplayName(this.toolName), true);
|
|
629
|
+
}
|
|
630
|
+
return originalMarkExecutionStarted.call(this);
|
|
631
|
+
};
|
|
632
|
+
const patchedToolRender = function (this: any, width: number): string[] {
|
|
633
|
+
if (this[renderedModeKey] !== renderMode) {
|
|
634
|
+
this[renderedModeKey] = renderMode;
|
|
635
|
+
this.updateDisplay();
|
|
636
|
+
}
|
|
637
|
+
return originalToolRender.call(this, width);
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
toolExecutionPrototype[toolExecutionPatchKey] = {
|
|
641
|
+
originalMarkExecutionStarted,
|
|
642
|
+
patchedMarkExecutionStarted,
|
|
643
|
+
originalRender: originalToolRender,
|
|
644
|
+
patchedRender: patchedToolRender,
|
|
645
|
+
};
|
|
646
|
+
toolExecutionPrototype.markExecutionStarted = patchedMarkExecutionStarted;
|
|
647
|
+
toolExecutionPrototype.render = patchedToolRender;
|
|
648
|
+
|
|
649
|
+
pi.on("session_shutdown", () => {
|
|
650
|
+
const patch = toolExecutionPrototype[toolExecutionPatchKey];
|
|
651
|
+
if (!patch) return;
|
|
652
|
+
if (patch.patchedMarkExecutionStarted === toolExecutionPrototype.markExecutionStarted) {
|
|
653
|
+
toolExecutionPrototype.markExecutionStarted = patch.originalMarkExecutionStarted;
|
|
654
|
+
}
|
|
655
|
+
if (patch.patchedRender === toolExecutionPrototype.render) {
|
|
656
|
+
toolExecutionPrototype.render = patch.originalRender;
|
|
657
|
+
}
|
|
658
|
+
if (
|
|
659
|
+
toolExecutionPrototype.markExecutionStarted === patch.originalMarkExecutionStarted &&
|
|
660
|
+
toolExecutionPrototype.render === patch.originalRender
|
|
661
|
+
) {
|
|
662
|
+
delete toolExecutionPrototype[toolExecutionPatchKey];
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const finishCleanGroup = (activity = cleanRun.activity ?? "done") => {
|
|
668
|
+
if (cleanRun.lastCompletedToolCallId && cleanRun.count > 0) {
|
|
669
|
+
const group: ToolSummaryGroup = {
|
|
670
|
+
count: cleanRun.count,
|
|
671
|
+
failed: cleanRun.failed,
|
|
672
|
+
lastToolCallId: cleanRun.lastCompletedToolCallId,
|
|
673
|
+
activity,
|
|
674
|
+
};
|
|
675
|
+
cleanRun.groups.push(group);
|
|
676
|
+
settledSummaries.set(group.lastToolCallId, {
|
|
677
|
+
count: group.count,
|
|
678
|
+
failed: group.failed,
|
|
679
|
+
activity: activityValueForHold(group.lastToolCallId, activity),
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
cleanRun.count = 0;
|
|
683
|
+
cleanRun.failed = 0;
|
|
684
|
+
cleanRun.lastCompletedToolCallId = undefined;
|
|
685
|
+
cleanRun.activeToolCallId = undefined;
|
|
686
|
+
cleanRun.activeToolName = undefined;
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const settleLastCleanGroup = () => {
|
|
690
|
+
const group = cleanRun.groups[cleanRun.groups.length - 1];
|
|
691
|
+
if (!group) return;
|
|
692
|
+
group.activity = "done";
|
|
693
|
+
settledSummaries.set(group.lastToolCallId, {
|
|
694
|
+
count: group.count,
|
|
695
|
+
failed: group.failed,
|
|
696
|
+
activity: activityValueForHold(group.lastToolCallId, "done"),
|
|
697
|
+
});
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const summaryText = (count: number, _failed: number, activity = "done"): string => {
|
|
701
|
+
const activityText = typeof activity === "string" ? activity : "done";
|
|
702
|
+
const countLabel = `${count} tool ${count === 1 ? "call" : "calls"}`;
|
|
703
|
+
const activityLabel = activityText !== "done" && activityText.trim()
|
|
704
|
+
? ` · ${activityText}`
|
|
705
|
+
: "";
|
|
706
|
+
return `${countLabel}${activityLabel}`;
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const summaryRow = (
|
|
710
|
+
theme: any,
|
|
711
|
+
count: number,
|
|
712
|
+
failed: number,
|
|
713
|
+
activity: DisplayValue = "done",
|
|
714
|
+
): DisplayRow => ({
|
|
715
|
+
prefix: () => {
|
|
716
|
+
const currentActivity = typeof activity === "function" ? activity() : activity;
|
|
717
|
+
const color = currentActivity === "done" ? "success" : "accent";
|
|
718
|
+
return theme.fg(color, "● ");
|
|
719
|
+
},
|
|
720
|
+
continuation: " ",
|
|
721
|
+
content: () => {
|
|
722
|
+
const currentActivity = typeof activity === "function" ? activity() : activity;
|
|
723
|
+
const label = currentActivity === "done" ? "Done" : "Running";
|
|
724
|
+
const color = label === "Done" ? "success" : "accent";
|
|
725
|
+
return theme.fg(color, theme.bold(label)) +
|
|
726
|
+
theme.fg("dim", "(") +
|
|
727
|
+
theme.fg("text", summaryText(count, failed, currentActivity)) + theme.fg("dim", ")");
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Clean mode keeps Running/Done rows in the existing tool components. This
|
|
733
|
+
* avoids waiting for agent_end (which can be followed by retry/compaction),
|
|
734
|
+
* leaves a visible count between calls, and keeps each row in transcript order.
|
|
735
|
+
*/
|
|
736
|
+
const cleanToolCall = (theme: any, name: string, toolCallId: string): Component => {
|
|
737
|
+
knownToolCallIds.add(toolCallId);
|
|
738
|
+
return {
|
|
739
|
+
render(width: number): string[] {
|
|
740
|
+
if (renderMode !== "clean") return [];
|
|
741
|
+
|
|
742
|
+
const settledSummary = settledSummaries.get(toolCallId);
|
|
743
|
+
if (settledSummary) {
|
|
744
|
+
return block([summaryRow(theme, settledSummary.count, settledSummary.failed, settledSummary.activity)]).render(width);
|
|
745
|
+
}
|
|
746
|
+
if (cleanRun.settled) return [];
|
|
747
|
+
|
|
748
|
+
if (cleanRun.activeToolCallId === toolCallId) {
|
|
749
|
+
return block([summaryRow(
|
|
750
|
+
theme,
|
|
751
|
+
cleanRun.count + 1,
|
|
752
|
+
cleanRun.failed,
|
|
753
|
+
() => cleanRun.activeToolName ?? name,
|
|
754
|
+
)]).render(width);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// The active tool owns the live Running row. Keep the latest completed
|
|
758
|
+
// portion hidden while that tool's minimum display time is running.
|
|
759
|
+
if (
|
|
760
|
+
cleanRun.lastCompletedToolCallId === toolCallId &&
|
|
761
|
+
cleanRun.count > 0 &&
|
|
762
|
+
!cleanRun.activeToolCallId
|
|
763
|
+
) {
|
|
764
|
+
return block([summaryRow(theme, cleanRun.count, cleanRun.failed, currentCleanActivity)]).render(width);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Do not fall back to the per-component pending state here: Pi may
|
|
768
|
+
// create several tool-call components before execution starts, and
|
|
769
|
+
// showing that fallback would briefly expose all of them. The active
|
|
770
|
+
// component is selected to render the current Running row at execution start.
|
|
771
|
+
return [];
|
|
772
|
+
},
|
|
773
|
+
invalidate() {},
|
|
774
|
+
};
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const hiddenToolResult = (context: any, options: any, output: string): Component => {
|
|
778
|
+
if (options.isPartial) setStatus(context, "running");
|
|
779
|
+
else completeStatus(context, output);
|
|
780
|
+
return block([]);
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
// In collapsed clean mode, every supported tool call is represented by its
|
|
784
|
+
// Running row. Ctrl+O bypasses this and restores the normal full renderer.
|
|
785
|
+
const hideCleanTool = (
|
|
786
|
+
_toolCallId: string,
|
|
787
|
+
expanded: boolean,
|
|
788
|
+
_executionStarted = false,
|
|
789
|
+
): boolean => renderMode === "clean" && !expanded;
|
|
790
|
+
|
|
791
|
+
pi.registerEntryRenderer<ToolSummaryData>("pretty-tui-tool-summary", (entry, { expanded }, theme) => ({
|
|
792
|
+
render(width: number): string[] {
|
|
793
|
+
if (renderMode !== "clean" || expanded) return [];
|
|
794
|
+
const data = entry.data;
|
|
795
|
+
const groups = data?.groups?.length
|
|
796
|
+
? data.groups
|
|
797
|
+
: data?.lastToolCallId
|
|
798
|
+
? [{
|
|
799
|
+
count: data.count ?? 0,
|
|
800
|
+
failed: data.failed ?? 0,
|
|
801
|
+
lastToolCallId: data.lastToolCallId,
|
|
802
|
+
}]
|
|
803
|
+
: (() => {
|
|
804
|
+
const summaryCallId = legacySummaryLastToolCallIds.get(entry.id);
|
|
805
|
+
return summaryCallId
|
|
806
|
+
? [{
|
|
807
|
+
count: data?.count ?? 0,
|
|
808
|
+
failed: data?.failed ?? 0,
|
|
809
|
+
lastToolCallId: summaryCallId,
|
|
810
|
+
}]
|
|
811
|
+
: [];
|
|
812
|
+
})();
|
|
813
|
+
// The live tool components own the visual positions. Keep this durable
|
|
814
|
+
// entry as a fallback only for groups whose tool components are absent
|
|
815
|
+
// from the current branch (for example, after compaction).
|
|
816
|
+
const missingGroups = groups.filter((group) => !knownToolCallIds.has(group.lastToolCallId));
|
|
817
|
+
if (missingGroups.length === 0) return [];
|
|
818
|
+
// Persisted entries represent settled groups. Older versions could
|
|
819
|
+
// accidentally store the transient responding... activity, so always
|
|
820
|
+
// normalize their display to Done.
|
|
821
|
+
return block(missingGroups.map((group) =>
|
|
822
|
+
summaryRow(theme, group.count, group.failed, "done")
|
|
823
|
+
)).render(width);
|
|
824
|
+
},
|
|
825
|
+
invalidate() {},
|
|
826
|
+
}));
|
|
827
|
+
|
|
828
|
+
pi.on("session_start", (_event, ctx) => {
|
|
829
|
+
cleanToolsExpanded = ctx.ui.getToolsExpanded();
|
|
830
|
+
settledSummaries.clear();
|
|
831
|
+
legacySummaryLastToolCallIds.clear();
|
|
832
|
+
clearToolActivityHolds();
|
|
833
|
+
cleanRun.requestRender = undefined;
|
|
834
|
+
cleanRun.count = 0;
|
|
835
|
+
cleanRun.failed = 0;
|
|
836
|
+
cleanRun.groups = [];
|
|
837
|
+
cleanRun.activeToolCallId = undefined;
|
|
838
|
+
cleanRun.activeToolName = undefined;
|
|
839
|
+
cleanRun.lastCompletedToolCallId = undefined;
|
|
840
|
+
cleanRun.activity = undefined;
|
|
841
|
+
cleanRun.active = false;
|
|
842
|
+
cleanRun.settled = false;
|
|
843
|
+
|
|
844
|
+
let count = 0;
|
|
845
|
+
let failed = 0;
|
|
846
|
+
let lastToolCallId: string | undefined;
|
|
847
|
+
let lastFinishedGroup: ToolSummaryGroup | undefined;
|
|
848
|
+
const toolCalls = new Set<string>();
|
|
849
|
+
const explicitSummaryIds = new Set<string>();
|
|
850
|
+
|
|
851
|
+
const rememberGroup = (group: ToolSummaryGroup, entryId?: string) => {
|
|
852
|
+
settledSummaries.set(group.lastToolCallId, {
|
|
853
|
+
count: group.count,
|
|
854
|
+
failed: group.failed,
|
|
855
|
+
activity: "done",
|
|
856
|
+
});
|
|
857
|
+
if (entryId) legacySummaryLastToolCallIds.set(entryId, group.lastToolCallId);
|
|
858
|
+
explicitSummaryIds.add(group.lastToolCallId);
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
const finishGroup = () => {
|
|
862
|
+
if (lastToolCallId && count > 0) {
|
|
863
|
+
lastFinishedGroup = { count, failed, lastToolCallId };
|
|
864
|
+
if (!explicitSummaryIds.has(lastToolCallId)) {
|
|
865
|
+
settledSummaries.set(lastToolCallId, { count, failed, activity: "done" });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
count = 0;
|
|
869
|
+
failed = 0;
|
|
870
|
+
lastToolCallId = undefined;
|
|
871
|
+
toolCalls.clear();
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
// Use the same compaction-aware branch that Pi renders in the transcript;
|
|
875
|
+
// getEntries() can also contain entries from other branches.
|
|
876
|
+
for (const entry of ctx.sessionManager.buildContextEntries()) {
|
|
877
|
+
if (entry.type === "custom" && entry.customType === "pretty-tui-tool-summary") {
|
|
878
|
+
const data = entry.data as ToolSummaryData | undefined;
|
|
879
|
+
if (data?.groups?.length) {
|
|
880
|
+
for (const group of data.groups) {
|
|
881
|
+
if (group?.lastToolCallId && group.count > 0) rememberGroup(group);
|
|
882
|
+
}
|
|
883
|
+
} else if (data?.lastToolCallId) {
|
|
884
|
+
rememberGroup({
|
|
885
|
+
count: data.count ?? 0,
|
|
886
|
+
failed: data.failed ?? 0,
|
|
887
|
+
lastToolCallId: data.lastToolCallId,
|
|
888
|
+
});
|
|
889
|
+
} else if (lastFinishedGroup) {
|
|
890
|
+
// Migrate summaries written by the earlier clean-mode versions,
|
|
891
|
+
// which did not persist the final tool call id. The text message
|
|
892
|
+
// preceding this entry has already closed the group.
|
|
893
|
+
rememberGroup({
|
|
894
|
+
count: data?.count ?? lastFinishedGroup.count,
|
|
895
|
+
failed: data?.failed ?? lastFinishedGroup.failed,
|
|
896
|
+
lastToolCallId: lastFinishedGroup.lastToolCallId,
|
|
897
|
+
}, entry.id);
|
|
898
|
+
} else if (lastToolCallId && count > 0) {
|
|
899
|
+
// Also handle a legacy entry inserted before the boundary text.
|
|
900
|
+
rememberGroup({
|
|
901
|
+
count: data?.count ?? count,
|
|
902
|
+
failed: data?.failed ?? failed,
|
|
903
|
+
lastToolCallId,
|
|
904
|
+
}, entry.id);
|
|
905
|
+
}
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (entry.type !== "message") continue;
|
|
910
|
+
const message = entry.message as any;
|
|
911
|
+
|
|
912
|
+
if (message.role === "user") {
|
|
913
|
+
finishGroup();
|
|
914
|
+
lastFinishedGroup = undefined;
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (message.role === "assistant") {
|
|
919
|
+
const hasVisibleText = (message.content ?? []).some(
|
|
920
|
+
(item: any) => item.type === "text" && typeof item.text === "string" && item.text.trim().length > 0,
|
|
921
|
+
);
|
|
922
|
+
if (hasVisibleText) finishGroup();
|
|
923
|
+
for (const item of message.content ?? []) {
|
|
924
|
+
if (item.type !== "toolCall" || !supportedTools.has(item.name)) continue;
|
|
925
|
+
count++;
|
|
926
|
+
lastToolCallId = item.id;
|
|
927
|
+
toolCalls.add(item.id);
|
|
928
|
+
}
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (message.role === "toolResult" && toolCalls.has(message.toolCallId) && message.isError) {
|
|
933
|
+
failed++;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
finishGroup();
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
const hasVisibleAssistantText = (message: any): boolean =>
|
|
941
|
+
message?.role === "assistant" && (message.content ?? []).some(
|
|
942
|
+
(item: any) => item.type === "text" && typeof item.text === "string" && item.text.trim().length > 0,
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
const latestPendingToolCall = (message: any): any =>
|
|
946
|
+
[...(message?.content ?? [])].reverse().find(
|
|
947
|
+
(item: any) => item.type === "toolCall" && supportedTools.has(item.name) && item.id,
|
|
948
|
+
);
|
|
949
|
+
|
|
950
|
+
const trackPendingToolActivity = (message: any): boolean => {
|
|
951
|
+
const toolCall = latestPendingToolCall(message);
|
|
952
|
+
if (!toolCall || cleanRun.lastCompletedToolCallId === toolCall.id) return false;
|
|
953
|
+
cleanRun.active = true;
|
|
954
|
+
cleanRun.activeToolCallId = toolCall.id;
|
|
955
|
+
beginToolActivity(toolCall.id, toolDisplayName(toolCall.name));
|
|
956
|
+
return true;
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
const clearPendingToolActivities = () => {
|
|
960
|
+
for (const [toolCallId, hold] of toolActivityHolds) {
|
|
961
|
+
if (hold.started) continue;
|
|
962
|
+
if (hold.timer) clearTimeout(hold.timer);
|
|
963
|
+
toolActivityHolds.delete(toolCallId);
|
|
964
|
+
if (cleanRun.activeToolCallId === toolCallId) {
|
|
965
|
+
cleanRun.activeToolCallId = undefined;
|
|
966
|
+
cleanRun.activeToolName = undefined;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
// A visible assistant response is the boundary between tool groups. Do
|
|
972
|
+
// this during streaming so a following tool call cannot inherit the prior
|
|
973
|
+
// summary, while message_end keeps the rule correct for non-streaming paths.
|
|
974
|
+
pi.on("message_update", (event) => {
|
|
975
|
+
if (hasVisibleAssistantText(event.message)) {
|
|
976
|
+
cleanRun.activity = "responding...";
|
|
977
|
+
finishCleanGroup("responding...");
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
trackPendingToolActivity(event.message);
|
|
981
|
+
});
|
|
982
|
+
pi.on("message_end", (event) => {
|
|
983
|
+
if (hasVisibleAssistantText(event.message)) {
|
|
984
|
+
cleanRun.activity = "done";
|
|
985
|
+
finishCleanGroup("done");
|
|
986
|
+
settleLastCleanGroup();
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
trackPendingToolActivity(event.message);
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
pi.on("agent_start", () => {
|
|
993
|
+
// A retry can start another low-level agent loop. Keep the accumulated
|
|
994
|
+
// count until the whole run reaches agent_settled.
|
|
995
|
+
if (!cleanRun.active) {
|
|
996
|
+
clearToolActivityHolds();
|
|
997
|
+
cleanRun.count = 0;
|
|
998
|
+
cleanRun.failed = 0;
|
|
999
|
+
cleanRun.groups = [];
|
|
1000
|
+
cleanRun.lastCompletedToolCallId = undefined;
|
|
1001
|
+
cleanRun.settled = false;
|
|
1002
|
+
}
|
|
1003
|
+
cleanRun.active = true;
|
|
1004
|
+
cleanRun.activeToolCallId = undefined;
|
|
1005
|
+
cleanRun.activeToolName = undefined;
|
|
1006
|
+
cleanRun.activity = "thinking...";
|
|
1007
|
+
});
|
|
1008
|
+
pi.on("tool_execution_start", (event) => {
|
|
1009
|
+
if (!supportedTools.has(event.toolName)) return;
|
|
1010
|
+
cleanRun.active = true;
|
|
1011
|
+
cleanRun.activeToolCallId = event.toolCallId;
|
|
1012
|
+
beginToolActivity(event.toolCallId, toolDisplayName(event.toolName), true);
|
|
1013
|
+
});
|
|
1014
|
+
pi.on("tool_execution_end", (event) => {
|
|
1015
|
+
if (!supportedTools.has(event.toolName)) return;
|
|
1016
|
+
const activityName = cleanRun.activeToolCallId === event.toolCallId
|
|
1017
|
+
? cleanRun.activeToolName ?? toolDisplayName(event.toolName)
|
|
1018
|
+
: toolDisplayName(event.toolName);
|
|
1019
|
+
// Keep the tool activity visible until at least one second after start;
|
|
1020
|
+
// this only delays the status transition, never the tool result itself.
|
|
1021
|
+
holdToolActivity(event.toolCallId, activityName);
|
|
1022
|
+
cleanRun.count++;
|
|
1023
|
+
if (event.isError) cleanRun.failed++;
|
|
1024
|
+
cleanRun.lastCompletedToolCallId = event.toolCallId;
|
|
1025
|
+
if (cleanRun.activeToolCallId === event.toolCallId) {
|
|
1026
|
+
cleanRun.activeToolCallId = undefined;
|
|
1027
|
+
cleanRun.activeToolName = undefined;
|
|
1028
|
+
cleanRun.activity = "thinking...";
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
pi.on("agent_end", () => {
|
|
1032
|
+
// No summary is appended here: this event may be followed by an automatic
|
|
1033
|
+
// retry or compaction. The live row remains available until settled.
|
|
1034
|
+
clearPendingToolActivities();
|
|
1035
|
+
cleanRun.activeToolCallId = undefined;
|
|
1036
|
+
cleanRun.activeToolName = undefined;
|
|
1037
|
+
cleanRun.activity = "thinking...";
|
|
1038
|
+
});
|
|
1039
|
+
pi.on("agent_settled", () => {
|
|
1040
|
+
if (cleanRun.settled) return;
|
|
1041
|
+
|
|
1042
|
+
clearPendingToolActivities();
|
|
1043
|
+
// Finalize the last group after retries/compaction have definitely ended.
|
|
1044
|
+
finishCleanGroup("done");
|
|
1045
|
+
for (const group of cleanRun.groups) {
|
|
1046
|
+
group.activity = "done";
|
|
1047
|
+
settledSummaries.set(group.lastToolCallId, {
|
|
1048
|
+
count: group.count,
|
|
1049
|
+
failed: group.failed,
|
|
1050
|
+
activity: activityValueForHold(group.lastToolCallId, "done"),
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
cleanRun.active = false;
|
|
1054
|
+
cleanRun.activeToolCallId = undefined;
|
|
1055
|
+
cleanRun.activeToolName = undefined;
|
|
1056
|
+
cleanRun.activity = "done";
|
|
1057
|
+
cleanRun.settled = true;
|
|
1058
|
+
|
|
1059
|
+
const groups = cleanRun.groups.slice();
|
|
1060
|
+
if (renderMode !== "clean" || groups.length === 0) return;
|
|
1061
|
+
|
|
1062
|
+
const count = groups.reduce((total, group) => total + group.count, 0);
|
|
1063
|
+
const failed = groups.reduce((total, group) => total + group.failed, 0);
|
|
1064
|
+
const lastToolCallId = groups[groups.length - 1]?.lastToolCallId;
|
|
1065
|
+
pi.appendEntry<ToolSummaryData>("pretty-tui-tool-summary", {
|
|
1066
|
+
count,
|
|
1067
|
+
failed,
|
|
1068
|
+
lastToolCallId,
|
|
1069
|
+
groups: groups.map(({ count, failed, lastToolCallId }) => ({ count, failed, lastToolCallId })),
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
|
|
170
1073
|
const read = createReadTool(cwd);
|
|
171
1074
|
pi.registerTool({
|
|
172
1075
|
...read,
|
|
173
1076
|
renderShell: "self",
|
|
174
1077
|
renderCall(args: any, theme: any, context: any) {
|
|
1078
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Read", context.toolCallId);
|
|
175
1079
|
const range = args.offset || args.limit
|
|
176
1080
|
? ` · lines ${args.offset ?? 1}${args.limit ? `–${(args.offset ?? 1) + args.limit - 1}` : "+"}`
|
|
177
1081
|
: "";
|
|
178
1082
|
return call(theme, "Read", `${args.path}${range}`, context.state);
|
|
179
1083
|
},
|
|
180
1084
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
181
|
-
if (options.isPartial) return partialResult(context, theme, "Reading…");
|
|
182
1085
|
const output = textContent(toolResult);
|
|
1086
|
+
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
1087
|
+
if (options.isPartial) return partialResult(context, theme, "Reading…");
|
|
183
1088
|
const image = toolResult.content?.find((item: any) => item.type === "image");
|
|
184
1089
|
const failed = completeStatus(context, output);
|
|
185
1090
|
if (image) return result(theme, "Read image", "", false, failed);
|
|
@@ -200,20 +1105,48 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
200
1105
|
...bash,
|
|
201
1106
|
renderShell: "self",
|
|
202
1107
|
renderCall(args: any, theme: any, context: any) {
|
|
203
|
-
|
|
1108
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Bash", context.toolCallId);
|
|
1109
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
1110
|
+
if (renderMode === "compact" && !context.expanded) {
|
|
1111
|
+
const commandLines = command.split(/\r\n|\r|\n/);
|
|
1112
|
+
const firstLine = commandLines[0] ?? "";
|
|
1113
|
+
const omitted = commandLines.length - 1;
|
|
1114
|
+
const detail = omitted > 0
|
|
1115
|
+
? `${firstLine} … (${omitted} more ${omitted === 1 ? "line" : "lines"})`
|
|
1116
|
+
: firstLine;
|
|
1117
|
+
return call(theme, "Bash", detail, context.state);
|
|
1118
|
+
}
|
|
204
1119
|
return call(theme, "Bash", command, context.state);
|
|
205
1120
|
},
|
|
206
1121
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
207
|
-
if (options.isPartial) return partialResult(context, theme, "Running…");
|
|
208
1122
|
const output = textContent(toolResult);
|
|
209
|
-
|
|
210
|
-
|
|
1123
|
+
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
1124
|
+
if (renderMode === "compact" && !options.expanded) {
|
|
1125
|
+
if (options.isPartial) return partialResult(context, theme, "Running…");
|
|
1126
|
+
const failed = completeStatus(context, output);
|
|
1127
|
+
return result(theme, failed ? "Command failed" : "Done", "", false, failed);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (options.isPartial) {
|
|
1131
|
+
return bashResult(context, theme, "Running…", output, options.expanded, "running");
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const failed = isError(context, output);
|
|
1135
|
+
const outputLines = terminalOutputLines(output);
|
|
1136
|
+
const lineCount = outputLines.filter((line) => line.length > 0).length;
|
|
211
1137
|
const summary = failed
|
|
212
|
-
?
|
|
213
|
-
:
|
|
214
|
-
? `Done · ${
|
|
1138
|
+
? outputLines[0] || "Command failed"
|
|
1139
|
+
: outputLines.length > 0
|
|
1140
|
+
? `Done · ${lineCount} output ${lineCount === 1 ? "line" : "lines"}`
|
|
215
1141
|
: "Done";
|
|
216
|
-
return
|
|
1142
|
+
return bashResult(
|
|
1143
|
+
context,
|
|
1144
|
+
theme,
|
|
1145
|
+
summary,
|
|
1146
|
+
output,
|
|
1147
|
+
options.expanded,
|
|
1148
|
+
failed ? "error" : "success",
|
|
1149
|
+
);
|
|
217
1150
|
},
|
|
218
1151
|
} as any);
|
|
219
1152
|
|
|
@@ -222,12 +1155,14 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
222
1155
|
...edit,
|
|
223
1156
|
renderShell: "self",
|
|
224
1157
|
renderCall(args: any, theme: any, context: any) {
|
|
1158
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Edit", context.toolCallId);
|
|
225
1159
|
const count = Array.isArray(args.edits) ? ` · ${args.edits.length} changes` : "";
|
|
226
1160
|
return call(theme, "Edit", `${args.path}${count}`, context.state);
|
|
227
1161
|
},
|
|
228
1162
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
229
|
-
if (options.isPartial) return partialResult(context, theme, "Editing…");
|
|
230
1163
|
const output = textContent(toolResult);
|
|
1164
|
+
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
1165
|
+
if (options.isPartial) return partialResult(context, theme, "Editing…");
|
|
231
1166
|
const failed = completeStatus(context, output);
|
|
232
1167
|
const diff = toolResult.details?.diff ?? "";
|
|
233
1168
|
const additions = diff.split("\n").filter((line: string) => line.startsWith("+") && !line.startsWith("+++")).length;
|
|
@@ -261,12 +1196,21 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
261
1196
|
...write,
|
|
262
1197
|
renderShell: "self",
|
|
263
1198
|
renderCall(args: any, theme: any, context: any) {
|
|
264
|
-
|
|
265
|
-
|
|
1199
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Write", context.toolCallId);
|
|
1200
|
+
const content = typeof args.content === "string" ? args.content : "";
|
|
1201
|
+
const path = String(args.path ?? "");
|
|
1202
|
+
if (renderMode === "compact" && !context.expanded) {
|
|
1203
|
+
const lines = content.replace(/\t/g, " ").split("\n");
|
|
1204
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
1205
|
+
const total = lines.length;
|
|
1206
|
+
return call(theme, "Write", `${path} · ${total} ${total === 1 ? "line" : "lines"}`, context.state);
|
|
1207
|
+
}
|
|
1208
|
+
return writeCall(theme, path, content, context.expanded, context.state);
|
|
266
1209
|
},
|
|
267
1210
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
268
|
-
if (options.isPartial) return partialResult(context, theme, "Writing…");
|
|
269
1211
|
const output = textContent(toolResult);
|
|
1212
|
+
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
1213
|
+
if (options.isPartial) return partialResult(context, theme, "Writing…");
|
|
270
1214
|
const failed = completeStatus(context, output);
|
|
271
1215
|
return result(
|
|
272
1216
|
theme,
|
|
@@ -283,13 +1227,15 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
283
1227
|
...grep,
|
|
284
1228
|
renderShell: "self",
|
|
285
1229
|
renderCall(args: any, theme: any, context: any) {
|
|
1230
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Grep", context.toolCallId);
|
|
286
1231
|
const where = args.path ? ` · ${args.path}` : "";
|
|
287
1232
|
const glob = args.glob ? ` · ${args.glob}` : "";
|
|
288
1233
|
return call(theme, "Grep", `${args.pattern}${where}${glob}`, context.state);
|
|
289
1234
|
},
|
|
290
1235
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
291
|
-
if (options.isPartial) return partialResult(context, theme, "Searching…");
|
|
292
1236
|
const output = textContent(toolResult);
|
|
1237
|
+
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
1238
|
+
if (options.isPartial) return partialResult(context, theme, "Searching…");
|
|
293
1239
|
const failed = completeStatus(context, output);
|
|
294
1240
|
const matches = nonEmptyLines(output);
|
|
295
1241
|
return result(theme, failed ? output.split("\n")[0] : `Found ${matches} matches`, output, options.expanded, failed);
|
|
@@ -301,11 +1247,13 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
301
1247
|
...find,
|
|
302
1248
|
renderShell: "self",
|
|
303
1249
|
renderCall(args: any, theme: any, context: any) {
|
|
1250
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Find", context.toolCallId);
|
|
304
1251
|
return call(theme, "Find", `${args.pattern}${args.path ? ` · ${args.path}` : ""}`, context.state);
|
|
305
1252
|
},
|
|
306
1253
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
307
|
-
if (options.isPartial) return partialResult(context, theme, "Searching…");
|
|
308
1254
|
const output = textContent(toolResult);
|
|
1255
|
+
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
1256
|
+
if (options.isPartial) return partialResult(context, theme, "Searching…");
|
|
309
1257
|
const failed = completeStatus(context, output);
|
|
310
1258
|
const matches = nonEmptyLines(output);
|
|
311
1259
|
return result(theme, failed ? output.split("\n")[0] : `Found ${matches} paths`, output, options.expanded, failed);
|
|
@@ -317,11 +1265,13 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
317
1265
|
...ls,
|
|
318
1266
|
renderShell: "self",
|
|
319
1267
|
renderCall(args: any, theme: any, context: any) {
|
|
1268
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "List", context.toolCallId);
|
|
320
1269
|
return call(theme, "List", args.path ?? ".", context.state);
|
|
321
1270
|
},
|
|
322
1271
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
323
|
-
if (options.isPartial) return partialResult(context, theme, "Listing…");
|
|
324
1272
|
const output = textContent(toolResult);
|
|
1273
|
+
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
1274
|
+
if (options.isPartial) return partialResult(context, theme, "Listing…");
|
|
325
1275
|
const failed = completeStatus(context, output);
|
|
326
1276
|
const entries = nonEmptyLines(output);
|
|
327
1277
|
return result(theme, failed ? output.split("\n")[0] : `Listed ${entries} entries`, output, options.expanded, failed);
|
package/package.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pretty-tui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Polished tool calls and list rendering for the Pi coding agent TUI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kainan Yang <ykn0309@whu.edu.cn>",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ykn0309/pi-pretty-tui.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/ykn0309/pi-pretty-tui#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ykn0309/pi-pretty-tui/issues"
|
|
15
|
+
},
|
|
8
16
|
"keywords": [
|
|
9
17
|
"pi-package",
|
|
10
18
|
"pi-extension",
|
|
@@ -32,6 +40,6 @@
|
|
|
32
40
|
"extensions": [
|
|
33
41
|
"./extensions/index.ts"
|
|
34
42
|
],
|
|
35
|
-
"image": "https://unpkg.com/pi-pretty-tui@0.1.
|
|
43
|
+
"image": "https://unpkg.com/pi-pretty-tui@0.1.4/assets/screenshot.png"
|
|
36
44
|
}
|
|
37
45
|
}
|