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 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
- - Gray running, green success, and red failure indicators
11
- - Concise result summaries with expandable output
12
- - Native-style `Write` previews: first 10 lines when collapsed, full content when expanded
13
- - Live `Bash` output: latest 5 lines when collapsed, all available output when expanded
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, and complete `Write` content.
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
 
Binary file
@@ -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
- return writeCall(theme, String(args.path ?? ""), content, context.expanded, context.state);
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",
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.3/assets/screenshot.png"
43
+ "image": "https://unpkg.com/pi-pretty-tui@0.1.4/assets/screenshot.png"
44
44
  }
45
45
  }