pi-pretty-tui 0.1.3 → 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 -6
- package/assets/screenshot.png +0 -0
- package/extensions/index.ts +848 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,10 +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
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
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
|
|
14
15
|
- Colored `+added` and `-removed` edit statistics and diffs
|
|
15
16
|
- Correct hanging indentation for long paths and wrapped tool output
|
|
16
17
|
- Bullet (`•`) markers for unordered Markdown lists
|
|
@@ -25,13 +26,32 @@ Restart Pi or run `/reload` after installation.
|
|
|
25
26
|
|
|
26
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.
|
|
27
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
|
+
|
|
28
48
|
## Expand tool output
|
|
29
49
|
|
|
30
|
-
Press `Ctrl+O` (Pi's default `app.tools.expand` keybinding) to show or hide detailed output, edit diffs,
|
|
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.
|
|
31
51
|
|
|
32
52
|
## Compatibility notice
|
|
33
53
|
|
|
34
|
-
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.
|
|
35
55
|
|
|
36
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.
|
|
37
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.
|
|
@@ -266,19 +468,623 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
266
468
|
return failed;
|
|
267
469
|
};
|
|
268
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
|
+
|
|
269
1073
|
const read = createReadTool(cwd);
|
|
270
1074
|
pi.registerTool({
|
|
271
1075
|
...read,
|
|
272
1076
|
renderShell: "self",
|
|
273
1077
|
renderCall(args: any, theme: any, context: any) {
|
|
1078
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Read", context.toolCallId);
|
|
274
1079
|
const range = args.offset || args.limit
|
|
275
1080
|
? ` · lines ${args.offset ?? 1}${args.limit ? `–${(args.offset ?? 1) + args.limit - 1}` : "+"}`
|
|
276
1081
|
: "";
|
|
277
1082
|
return call(theme, "Read", `${args.path}${range}`, context.state);
|
|
278
1083
|
},
|
|
279
1084
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
280
|
-
if (options.isPartial) return partialResult(context, theme, "Reading…");
|
|
281
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…");
|
|
282
1088
|
const image = toolResult.content?.find((item: any) => item.type === "image");
|
|
283
1089
|
const failed = completeStatus(context, output);
|
|
284
1090
|
if (image) return result(theme, "Read image", "", false, failed);
|
|
@@ -299,11 +1105,28 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
299
1105
|
...bash,
|
|
300
1106
|
renderShell: "self",
|
|
301
1107
|
renderCall(args: any, theme: any, context: any) {
|
|
1108
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Bash", context.toolCallId);
|
|
302
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
|
+
}
|
|
303
1119
|
return call(theme, "Bash", command, context.state);
|
|
304
1120
|
},
|
|
305
1121
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
306
1122
|
const output = textContent(toolResult);
|
|
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
|
+
|
|
307
1130
|
if (options.isPartial) {
|
|
308
1131
|
return bashResult(context, theme, "Running…", output, options.expanded, "running");
|
|
309
1132
|
}
|
|
@@ -332,12 +1155,14 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
332
1155
|
...edit,
|
|
333
1156
|
renderShell: "self",
|
|
334
1157
|
renderCall(args: any, theme: any, context: any) {
|
|
1158
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Edit", context.toolCallId);
|
|
335
1159
|
const count = Array.isArray(args.edits) ? ` · ${args.edits.length} changes` : "";
|
|
336
1160
|
return call(theme, "Edit", `${args.path}${count}`, context.state);
|
|
337
1161
|
},
|
|
338
1162
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
339
|
-
if (options.isPartial) return partialResult(context, theme, "Editing…");
|
|
340
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…");
|
|
341
1166
|
const failed = completeStatus(context, output);
|
|
342
1167
|
const diff = toolResult.details?.diff ?? "";
|
|
343
1168
|
const additions = diff.split("\n").filter((line: string) => line.startsWith("+") && !line.startsWith("+++")).length;
|
|
@@ -371,12 +1196,21 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
371
1196
|
...write,
|
|
372
1197
|
renderShell: "self",
|
|
373
1198
|
renderCall(args: any, theme: any, context: any) {
|
|
1199
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Write", context.toolCallId);
|
|
374
1200
|
const content = typeof args.content === "string" ? args.content : "";
|
|
375
|
-
|
|
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);
|
|
376
1209
|
},
|
|
377
1210
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
378
|
-
if (options.isPartial) return partialResult(context, theme, "Writing…");
|
|
379
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…");
|
|
380
1214
|
const failed = completeStatus(context, output);
|
|
381
1215
|
return result(
|
|
382
1216
|
theme,
|
|
@@ -393,13 +1227,15 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
393
1227
|
...grep,
|
|
394
1228
|
renderShell: "self",
|
|
395
1229
|
renderCall(args: any, theme: any, context: any) {
|
|
1230
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Grep", context.toolCallId);
|
|
396
1231
|
const where = args.path ? ` · ${args.path}` : "";
|
|
397
1232
|
const glob = args.glob ? ` · ${args.glob}` : "";
|
|
398
1233
|
return call(theme, "Grep", `${args.pattern}${where}${glob}`, context.state);
|
|
399
1234
|
},
|
|
400
1235
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
401
|
-
if (options.isPartial) return partialResult(context, theme, "Searching…");
|
|
402
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…");
|
|
403
1239
|
const failed = completeStatus(context, output);
|
|
404
1240
|
const matches = nonEmptyLines(output);
|
|
405
1241
|
return result(theme, failed ? output.split("\n")[0] : `Found ${matches} matches`, output, options.expanded, failed);
|
|
@@ -411,11 +1247,13 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
411
1247
|
...find,
|
|
412
1248
|
renderShell: "self",
|
|
413
1249
|
renderCall(args: any, theme: any, context: any) {
|
|
1250
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Find", context.toolCallId);
|
|
414
1251
|
return call(theme, "Find", `${args.pattern}${args.path ? ` · ${args.path}` : ""}`, context.state);
|
|
415
1252
|
},
|
|
416
1253
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
417
|
-
if (options.isPartial) return partialResult(context, theme, "Searching…");
|
|
418
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…");
|
|
419
1257
|
const failed = completeStatus(context, output);
|
|
420
1258
|
const matches = nonEmptyLines(output);
|
|
421
1259
|
return result(theme, failed ? output.split("\n")[0] : `Found ${matches} paths`, output, options.expanded, failed);
|
|
@@ -427,11 +1265,13 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
427
1265
|
...ls,
|
|
428
1266
|
renderShell: "self",
|
|
429
1267
|
renderCall(args: any, theme: any, context: any) {
|
|
1268
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "List", context.toolCallId);
|
|
430
1269
|
return call(theme, "List", args.path ?? ".", context.state);
|
|
431
1270
|
},
|
|
432
1271
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
433
|
-
if (options.isPartial) return partialResult(context, theme, "Listing…");
|
|
434
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…");
|
|
435
1275
|
const failed = completeStatus(context, output);
|
|
436
1276
|
const entries = nonEmptyLines(output);
|
|
437
1277
|
return result(theme, failed ? output.split("\n")[0] : `Listed ${entries} entries`, output, options.expanded, failed);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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>",
|
|
@@ -40,6 +40,6 @@
|
|
|
40
40
|
"extensions": [
|
|
41
41
|
"./extensions/index.ts"
|
|
42
42
|
],
|
|
43
|
-
"image": "https://unpkg.com/pi-pretty-tui@0.1.
|
|
43
|
+
"image": "https://unpkg.com/pi-pretty-tui@0.1.4/assets/screenshot.png"
|
|
44
44
|
}
|
|
45
45
|
}
|