@diegopetrucci/pi-extensions 0.1.74 → 0.1.78

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.
Files changed (108) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +17 -12
  3. package/extensions/agent-workflow-audit/README.md +1 -1
  4. package/extensions/agent-workflow-audit/index.ts +1631 -1009
  5. package/extensions/agent-workflow-audit/package.json +1 -1
  6. package/extensions/annotate-git-diff/README.md +3 -1
  7. package/extensions/annotate-git-diff/clipboard.ts +109 -100
  8. package/extensions/annotate-git-diff/git.ts +846 -768
  9. package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
  10. package/extensions/annotate-git-diff/index.ts +712 -671
  11. package/extensions/annotate-git-diff/package.json +1 -1
  12. package/extensions/annotate-git-diff/prompt.ts +45 -45
  13. package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
  14. package/extensions/annotate-git-diff/review-server.ts +337 -311
  15. package/extensions/annotate-git-diff/types.ts +132 -132
  16. package/extensions/annotate-git-diff/ui.ts +27 -27
  17. package/extensions/annotate-git-diff/watch.ts +74 -74
  18. package/extensions/annotate-git-diff/web/app.js +1947 -1884
  19. package/extensions/annotate-git-diff/web/index.html +5 -6
  20. package/extensions/annotate-last-message/README.md +1 -1
  21. package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
  22. package/extensions/annotate-last-message/index.ts +210 -206
  23. package/extensions/annotate-last-message/package.json +1 -1
  24. package/extensions/annotate-last-message/prompt.ts +87 -66
  25. package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
  26. package/extensions/annotate-last-message/session.ts +94 -89
  27. package/extensions/annotate-last-message/theme.ts +199 -193
  28. package/extensions/annotate-last-message/types.ts +23 -23
  29. package/extensions/annotate-last-message/ui.ts +27 -25
  30. package/extensions/annotate-last-message/web/app.js +4 -7
  31. package/extensions/annotate-last-message/web/index.html +3 -3
  32. package/extensions/annotate-last-message/web/md-renderer.js +1 -1
  33. package/extensions/brrr/README.md +1 -1
  34. package/extensions/brrr/index.ts +213 -191
  35. package/extensions/brrr/package.json +1 -1
  36. package/extensions/claude-fast/README.md +5 -79
  37. package/extensions/claude-fast/index.ts +31 -289
  38. package/extensions/claude-fast/package.json +2 -3
  39. package/extensions/code-reviewer/README.md +6 -6
  40. package/extensions/code-reviewer/index.ts +1825 -1373
  41. package/extensions/code-reviewer/package.json +1 -1
  42. package/extensions/confirm-destructive/README.md +1 -1
  43. package/extensions/confirm-destructive/index.ts +50 -52
  44. package/extensions/confirm-destructive/package.json +1 -1
  45. package/extensions/context-cap/README.md +9 -9
  46. package/extensions/context-cap/index.ts +313 -305
  47. package/extensions/context-cap/package.json +2 -2
  48. package/extensions/context-inspector/index.ts +1211 -1064
  49. package/extensions/context-inspector/package.json +1 -1
  50. package/extensions/contrarian/README.md +4 -3
  51. package/extensions/contrarian/index.ts +1863 -1737
  52. package/extensions/contrarian/package.json +1 -1
  53. package/extensions/dirty-repo-guard/index.ts +37 -41
  54. package/extensions/dirty-repo-guard/package.json +1 -1
  55. package/extensions/dynamic-context-pruning/README.md +11 -43
  56. package/extensions/dynamic-context-pruning/index.ts +2131 -2105
  57. package/extensions/dynamic-context-pruning/package.json +1 -1
  58. package/extensions/fast/README.md +52 -14
  59. package/extensions/fast/index.ts +513 -317
  60. package/extensions/fast/package.json +1 -1
  61. package/extensions/git-footer/README.md +1 -1
  62. package/extensions/git-footer/index.ts +458 -411
  63. package/extensions/git-footer/package.json +2 -3
  64. package/extensions/gnosis/index.ts +180 -130
  65. package/extensions/gnosis/package.json +1 -1
  66. package/extensions/inline-bash/index.ts +87 -103
  67. package/extensions/inline-bash/package.json +1 -1
  68. package/extensions/librarian/README.md +1 -1
  69. package/extensions/librarian/index.ts +1877 -1419
  70. package/extensions/librarian/package.json +1 -1
  71. package/extensions/minimal-footer/README.md +5 -5
  72. package/extensions/minimal-footer/index.ts +987 -1012
  73. package/extensions/minimal-footer/minimal-footer.example.json +1 -1
  74. package/extensions/minimal-footer/openai-usage.ts +195 -190
  75. package/extensions/minimal-footer/package.json +1 -1
  76. package/extensions/notify/index.ts +222 -230
  77. package/extensions/notify/package.json +1 -1
  78. package/extensions/openai-fast/README.md +5 -73
  79. package/extensions/openai-fast/index.ts +32 -272
  80. package/extensions/openai-fast/package.json +2 -3
  81. package/extensions/oracle/README.md +4 -3
  82. package/extensions/oracle/index.ts +1740 -1627
  83. package/extensions/oracle/package.json +1 -1
  84. package/extensions/permission-gate/README.md +3 -0
  85. package/extensions/permission-gate/index.ts +785 -612
  86. package/extensions/permission-gate/package.json +1 -1
  87. package/extensions/permission-gate/powershell-safety.ts +840 -793
  88. package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
  89. package/extensions/project-mcp-json/README.md +199 -0
  90. package/extensions/project-mcp-json/config.ts +564 -0
  91. package/extensions/project-mcp-json/index.ts +119 -0
  92. package/extensions/project-mcp-json/package.json +38 -0
  93. package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
  94. package/extensions/quiet-tools/README.md +30 -19
  95. package/extensions/quiet-tools/index.ts +376 -291
  96. package/extensions/quiet-tools/package.json +2 -2
  97. package/extensions/review/README.md +2 -2
  98. package/extensions/review/index.ts +1745 -1872
  99. package/extensions/review/package.json +1 -1
  100. package/extensions/todo/index.ts +280 -280
  101. package/extensions/todo/package.json +1 -1
  102. package/extensions/triage-comments/README.md +6 -4
  103. package/extensions/triage-comments/index.ts +2620 -1888
  104. package/extensions/triage-comments/package.json +1 -1
  105. package/package.json +9 -6
  106. package/assets/social-preview.png +0 -0
  107. package/extensions/claude-fast/claude-fast.example.json +0 -4
  108. package/extensions/openai-fast/openai-fast.example.json +0 -4
@@ -1,377 +1,462 @@
1
1
  import { homedir } from "node:os";
2
2
 
3
3
  import {
4
- createBashToolDefinition,
5
- createEditToolDefinition,
6
- createFindToolDefinition,
7
- createGrepToolDefinition,
8
- createLsToolDefinition,
9
- createReadToolDefinition,
10
- createWriteToolDefinition,
11
- keyHint,
12
- SettingsManager,
13
- type ExtensionAPI,
14
- type ToolDefinition as PiToolDefinition,
15
- type ToolsOptions,
4
+ keyHint,
5
+ type ExtensionAPI,
6
+ type ToolRendererResolver,
7
+ type ToolRenderers,
16
8
  } from "@earendil-works/pi-coding-agent";
17
- import { Container, Text, truncateToWidth } from "@earendil-works/pi-tui";
9
+ import { Box, Container, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
18
10
 
19
- const QUIET_CALL_TOOL_NAMES = new Set(["bash", "edit", "find", "grep", "ls", "read", "write"]);
11
+ const QUIET_TOOL_NAMES = new Set(["bash", "edit", "find", "grep", "ls", "read", "write"]);
12
+ const MAX_MCP_ARG_LENGTH = 2000;
20
13
 
21
- type ToolDefinition = PiToolDefinition<any, any, any>;
22
- type ToolRenderCall = NonNullable<ToolDefinition["renderCall"]>;
23
- type ToolRenderResult = NonNullable<ToolDefinition["renderResult"]>;
14
+ type ToolRenderCall = NonNullable<ToolRenderers["renderCall"]>;
15
+ type ToolRenderResult = NonNullable<ToolRenderers["renderResult"]>;
24
16
  type ToolRenderCallParams = Parameters<ToolRenderCall>;
25
17
  type ToolRenderResultParams = Parameters<ToolRenderResult>;
26
- type RenderTheme = ToolRenderResultParams[2];
27
- type ToolRenderContext = ToolRenderResultParams[3];
28
-
29
- type TimerRenderState = {
30
- startedAt?: number;
31
- endedAt?: number;
32
- interval?: ReturnType<typeof setInterval>;
18
+ type RenderTheme = ToolRenderCallParams[1];
19
+ type ToolRenderContext = ToolRenderCallParams[2];
20
+ type QuietToolRenderContext = ToolRenderContext & {
21
+ durationMs?: number;
22
+ outputPad?: number;
23
+ };
24
+ type BashRenderState = {
25
+ startedAt?: number;
26
+ endedAt?: number;
27
+ interval?: ReturnType<typeof setInterval>;
33
28
  };
34
29
 
30
+ type QuietLinesRenderer = (width: number) => string[];
31
+
35
32
  class QuietLinesRenderComponent extends Container {
36
- private linesRenderer: ((width: number) => string[]) | undefined;
33
+ private linesRenderer: QuietLinesRenderer | undefined;
37
34
 
38
- setLinesRenderer(linesRenderer: (width: number) => string[]): void {
39
- this.linesRenderer = linesRenderer;
40
- this.invalidate();
41
- }
35
+ setLinesRenderer(linesRenderer: QuietLinesRenderer): void {
36
+ this.linesRenderer = linesRenderer;
37
+ this.invalidate();
38
+ }
39
+
40
+ protected renderLines(width: number): string[] {
41
+ return this.linesRenderer?.(width).filter((line) => line) ?? [];
42
+ }
43
+
44
+ render(width: number): string[] {
45
+ return this.renderLines(width);
46
+ }
47
+ }
42
48
 
43
- render(width: number): string[] {
44
- return this.linesRenderer?.(width).filter((line) => line) ?? [];
45
- }
49
+ class QuietCallRenderComponent extends QuietLinesRenderComponent {
50
+ private frame: Box | undefined;
51
+ private framePaddingX: number | undefined;
52
+
53
+ private readonly frameBody = {
54
+ render: (width: number) => this.renderLines(width),
55
+ invalidate: () => {},
56
+ };
57
+
58
+ setNativeFrame(theme: RenderTheme, context: QuietToolRenderContext): void {
59
+ const outputPad = getOutputPad(context);
60
+ if (!this.frame || this.framePaddingX !== outputPad) {
61
+ this.frame = new Box(outputPad, 1);
62
+ this.framePaddingX = outputPad;
63
+ }
64
+
65
+ const themeWithBackground = theme as RenderTheme & {
66
+ bg?: (kind: string, text: string) => string;
67
+ };
68
+ const backgroundKind = context.isPartial
69
+ ? "toolPendingBg"
70
+ : context.isError
71
+ ? "toolErrorBg"
72
+ : "toolSuccessBg";
73
+ const background = themeWithBackground.bg;
74
+ this.frame.setBgFn(
75
+ background ? (text) => background.call(themeWithBackground, backgroundKind, text) : undefined,
76
+ );
77
+ }
78
+
79
+ clearNativeFrame(): void {
80
+ this.frame = undefined;
81
+ this.framePaddingX = undefined;
82
+ }
83
+
84
+ render(width: number): string[] {
85
+ if (!this.frame) return super.render(width);
86
+
87
+ this.frame.clear();
88
+ this.frame.addChild(this.frameBody);
89
+ return this.frame.render(width);
90
+ }
91
+
92
+ invalidate(): void {
93
+ super.invalidate();
94
+ this.frame?.invalidate();
95
+ }
46
96
  }
47
97
 
48
- class QuietCallRenderComponent extends QuietLinesRenderComponent {}
49
98
  class QuietResultRenderComponent extends QuietLinesRenderComponent {}
50
99
 
100
+ /**
101
+ * Thrown from a delegating renderCall/renderResult slot when next() did not supply that slot.
102
+ * Pi's ToolExecutionComponent catches renderer errors and falls back to its native
103
+ * createCallFallback/createResultFallback, which preserve the host's expanded behavior.
104
+ */
105
+ class QuietToolsDelegateToDefault extends Error {
106
+ constructor() {
107
+ super("quiet-tools: delegate to Pi default renderer");
108
+ this.name = "QuietToolsDelegateToDefault";
109
+ }
110
+ }
111
+
51
112
  function sanitizeInlineText(text: string): string {
52
- return text
53
- .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
54
- .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
55
- .replace(/[\r\n\t]+/g, " ")
56
- .replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "");
113
+ return text
114
+ .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
115
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
116
+ .replace(/[\r\n\t]+/g, " ")
117
+ .replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "");
57
118
  }
58
119
 
59
120
  function str(value: unknown): string | null {
60
- if (typeof value === "string") return sanitizeInlineText(value);
61
- if (value == null) return "";
62
- return null;
121
+ if (typeof value === "string") return sanitizeInlineText(value);
122
+ if (value == null) return "";
123
+ return null;
63
124
  }
64
125
 
65
126
  function asRecord(args: unknown): Record<string, unknown> | undefined {
66
- return args && typeof args === "object" ? (args as Record<string, unknown>) : undefined;
127
+ return args && typeof args === "object" ? (args as Record<string, unknown>) : undefined;
67
128
  }
68
129
 
69
130
  function firstStringArg(args: unknown, names: string[]): string | null {
70
- const record = asRecord(args);
71
- if (!record) return "";
72
- for (const name of names) {
73
- if (Object.prototype.hasOwnProperty.call(record, name)) {
74
- return str(record[name]);
75
- }
76
- }
77
- return "";
131
+ const record = asRecord(args);
132
+ if (!record) return "";
133
+ for (const name of names) {
134
+ if (Object.prototype.hasOwnProperty.call(record, name)) {
135
+ return str(record[name]);
136
+ }
137
+ }
138
+ return "";
78
139
  }
79
140
 
80
141
  function numberArg(args: unknown, name: string): number | undefined {
81
- const value = asRecord(args)?.[name];
82
- return typeof value === "number" ? value : undefined;
142
+ const value = asRecord(args)?.[name];
143
+ return typeof value === "number" ? value : undefined;
83
144
  }
84
145
 
85
146
  function invalidArgText(theme: RenderTheme): string {
86
- return theme.fg("error", "[invalid arg]");
147
+ return theme.fg("error", "[invalid arg]");
87
148
  }
88
149
 
89
150
  function shortenPath(path: string): string {
90
- const home = homedir();
91
- return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
151
+ const home = homedir();
152
+ return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
92
153
  }
93
154
 
94
155
  function formatToolTitle(toolName: string, theme: RenderTheme): string {
95
- return theme.fg("toolTitle", theme.bold(toolName));
156
+ return theme.fg("toolTitle", theme.bold(toolName));
96
157
  }
97
158
 
98
159
  function formatPathArg(args: unknown, theme: RenderTheme, placeholder = "..."): string {
99
- const path = firstStringArg(args, ["file_path", "path"]);
100
- if (path === null) return invalidArgText(theme);
101
- return path ? theme.fg("accent", shortenPath(path)) : theme.fg("toolOutput", placeholder);
160
+ const path = firstStringArg(args, ["file_path", "path"]);
161
+ if (path === null) return invalidArgText(theme);
162
+ return path ? theme.fg("accent", shortenPath(path)) : theme.fg("toolOutput", placeholder);
102
163
  }
103
164
 
104
165
  function formatReadLineRange(args: unknown, theme: RenderTheme): string {
105
- const offset = numberArg(args, "offset");
106
- const limit = numberArg(args, "limit");
107
- if (offset === undefined && limit === undefined) return "";
108
- const startLine = offset ?? 1;
109
- const endLine = limit !== undefined ? startLine + limit - 1 : "";
110
- return theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
166
+ const offset = numberArg(args, "offset");
167
+ const limit = numberArg(args, "limit");
168
+ if (offset === undefined && limit === undefined) return "";
169
+ const startLine = offset ?? 1;
170
+ const endLine = limit !== undefined ? startLine + limit - 1 : "";
171
+ return theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
111
172
  }
112
173
 
113
174
  function formatQuietReadCall(args: unknown, theme: RenderTheme): string {
114
- return `${formatToolTitle("read", theme)} ${formatPathArg(args, theme)}${formatReadLineRange(args, theme)}`;
175
+ return `${formatToolTitle("read", theme)} ${formatPathArg(args, theme)}${formatReadLineRange(args, theme)}`;
115
176
  }
116
177
 
117
178
  function formatQuietBashCall(args: unknown, theme: RenderTheme): string {
118
- const command = str(asRecord(args)?.command);
119
- const timeout = numberArg(args, "timeout");
120
- const commandDisplay = command === null
121
- ? invalidArgText(theme)
122
- : command
123
- ? theme.fg("toolTitle", theme.bold(command))
124
- : theme.fg("toolOutput", "...");
125
- const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
126
- return `${theme.fg("toolTitle", theme.bold("$"))} ${commandDisplay}${timeoutSuffix}`;
179
+ const command = str(asRecord(args)?.command);
180
+ const timeout = numberArg(args, "timeout");
181
+ const commandDisplay =
182
+ command === null
183
+ ? invalidArgText(theme)
184
+ : command
185
+ ? theme.fg("toolTitle", theme.bold(command))
186
+ : theme.fg("toolOutput", "...");
187
+ const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
188
+ return `${theme.fg("toolTitle", theme.bold("$"))} ${commandDisplay}${timeoutSuffix}`;
127
189
  }
128
190
 
129
191
  function formatSearchPath(rawPath: string | null, theme: RenderTheme): string {
130
- return rawPath === null ? invalidArgText(theme) : shortenPath(rawPath || ".");
192
+ return rawPath === null ? invalidArgText(theme) : shortenPath(rawPath || ".");
131
193
  }
132
194
 
133
195
  function formatQuietGrepCall(args: unknown, theme: RenderTheme): string {
134
- const pattern = str(asRecord(args)?.pattern);
135
- const rawPath = str(asRecord(args)?.path);
136
- const glob = str(asRecord(args)?.glob);
137
- const limit = numberArg(args, "limit");
138
- let text = `${formatToolTitle("grep", theme)} ${
139
- pattern === null ? invalidArgText(theme) : theme.fg("accent", `/${pattern || ""}/`)
140
- }${theme.fg("toolOutput", ` in ${formatSearchPath(rawPath, theme)}`)}`;
141
- if (glob) text += theme.fg("toolOutput", ` (${glob})`);
142
- if (glob === null) text += ` ${invalidArgText(theme)}`;
143
- if (limit !== undefined) text += theme.fg("toolOutput", ` limit ${limit}`);
144
- return text;
196
+ const pattern = str(asRecord(args)?.pattern);
197
+ const rawPath = str(asRecord(args)?.path);
198
+ const glob = str(asRecord(args)?.glob);
199
+ const limit = numberArg(args, "limit");
200
+ let text = `${formatToolTitle("grep", theme)} ${
201
+ pattern === null ? invalidArgText(theme) : theme.fg("accent", `/${pattern || ""}/`)
202
+ }${theme.fg("toolOutput", ` in ${formatSearchPath(rawPath, theme)}`)}`;
203
+ if (glob) text += theme.fg("toolOutput", ` (${glob})`);
204
+ if (glob === null) text += ` ${invalidArgText(theme)}`;
205
+ if (limit !== undefined) text += theme.fg("toolOutput", ` limit ${limit}`);
206
+ return text;
145
207
  }
146
208
 
147
209
  function formatQuietFindCall(args: unknown, theme: RenderTheme): string {
148
- const pattern = str(asRecord(args)?.pattern);
149
- const rawPath = str(asRecord(args)?.path);
150
- const limit = numberArg(args, "limit");
151
- let text = `${formatToolTitle("find", theme)} ${
152
- pattern === null ? invalidArgText(theme) : theme.fg("accent", pattern || "")
153
- }${theme.fg("toolOutput", ` in ${formatSearchPath(rawPath, theme)}`)}`;
154
- if (limit !== undefined) text += theme.fg("toolOutput", ` (limit ${limit})`);
155
- return text;
210
+ const pattern = str(asRecord(args)?.pattern);
211
+ const rawPath = str(asRecord(args)?.path);
212
+ const limit = numberArg(args, "limit");
213
+ let text = `${formatToolTitle("find", theme)} ${
214
+ pattern === null ? invalidArgText(theme) : theme.fg("accent", pattern || "")
215
+ }${theme.fg("toolOutput", ` in ${formatSearchPath(rawPath, theme)}`)}`;
216
+ if (limit !== undefined) text += theme.fg("toolOutput", ` (limit ${limit})`);
217
+ return text;
156
218
  }
157
219
 
158
220
  function formatQuietLsCall(args: unknown, theme: RenderTheme): string {
159
- const rawPath = str(asRecord(args)?.path);
160
- const limit = numberArg(args, "limit");
161
- let text = `${formatToolTitle("ls", theme)} ${
162
- rawPath === null ? invalidArgText(theme) : theme.fg("accent", shortenPath(rawPath || "."))
163
- }`;
164
- if (limit !== undefined) text += theme.fg("toolOutput", ` (limit ${limit})`);
165
- return text;
221
+ const rawPath = str(asRecord(args)?.path);
222
+ const limit = numberArg(args, "limit");
223
+ let text = `${formatToolTitle("ls", theme)} ${
224
+ rawPath === null ? invalidArgText(theme) : theme.fg("accent", shortenPath(rawPath || "."))
225
+ }`;
226
+ if (limit !== undefined) text += theme.fg("toolOutput", ` (limit ${limit})`);
227
+ return text;
166
228
  }
167
229
 
168
- function formatQuietPathOnlyCall(toolName: "edit" | "write", args: unknown, theme: RenderTheme): string {
169
- return `${formatToolTitle(toolName, theme)} ${formatPathArg(args, theme)}`;
230
+ function formatQuietPathOnlyCall(
231
+ toolName: "edit" | "write",
232
+ args: unknown,
233
+ theme: RenderTheme,
234
+ ): string {
235
+ return `${formatToolTitle(toolName, theme)} ${formatPathArg(args, theme)}`;
170
236
  }
171
237
 
172
238
  function formatQuietCallLine(toolName: string, args: unknown, theme: RenderTheme): string {
173
- switch (toolName) {
174
- case "bash":
175
- return formatQuietBashCall(args, theme);
176
- case "edit":
177
- return formatQuietPathOnlyCall("edit", args, theme);
178
- case "find":
179
- return formatQuietFindCall(args, theme);
180
- case "grep":
181
- return formatQuietGrepCall(args, theme);
182
- case "ls":
183
- return formatQuietLsCall(args, theme);
184
- case "read":
185
- return formatQuietReadCall(args, theme);
186
- case "write":
187
- return formatQuietPathOnlyCall("write", args, theme);
188
- default:
189
- return formatToolTitle(toolName, theme);
190
- }
239
+ switch (toolName) {
240
+ case "bash":
241
+ return formatQuietBashCall(args, theme);
242
+ case "edit":
243
+ return formatQuietPathOnlyCall("edit", args, theme);
244
+ case "find":
245
+ return formatQuietFindCall(args, theme);
246
+ case "grep":
247
+ return formatQuietGrepCall(args, theme);
248
+ case "ls":
249
+ return formatQuietLsCall(args, theme);
250
+ case "read":
251
+ return formatQuietReadCall(args, theme);
252
+ case "write":
253
+ return formatQuietPathOnlyCall("write", args, theme);
254
+ default:
255
+ return formatToolTitle(toolName, theme);
256
+ }
257
+ }
258
+
259
+ function formatMcpCallLine(toolName: string, args: unknown, theme: RenderTheme): string {
260
+ let argsStr = "";
261
+ if (args != null) {
262
+ let raw: string | undefined;
263
+ try {
264
+ raw = JSON.stringify(args);
265
+ } catch {
266
+ // circular references, BigInt, etc.
267
+ }
268
+ if (raw === undefined) {
269
+ argsStr = "[\u2026]";
270
+ } else {
271
+ const capped = raw.length > MAX_MCP_ARG_LENGTH ? raw.slice(0, MAX_MCP_ARG_LENGTH) : raw;
272
+ argsStr = sanitizeInlineText(capped);
273
+ }
274
+ }
275
+ return `${formatToolTitle(toolName, theme)}${argsStr ? ` ${theme.fg("toolOutput", argsStr)}` : ""}`;
191
276
  }
192
277
 
193
278
  function formatExpandHint(theme: RenderTheme): string {
194
- return `${theme.fg("muted", "(")}${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
279
+ return `${theme.fg("muted", "(")}${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
195
280
  }
196
281
 
197
- function markToolTiming(options: ToolRenderResultParams[1], context: ToolRenderContext): void {
198
- const state = context.state as TimerRenderState;
282
+ function formatFailedMarker(theme: RenderTheme): string {
283
+ return theme.fg("error", " [failed]");
284
+ }
199
285
 
200
- if ((!options.isPartial || context.isError) && state.startedAt !== undefined) {
201
- state.endedAt ??= Date.now();
202
- }
286
+ function truncateQuietCallLine(line: string, status: string, width: number): string {
287
+ if (!status) return truncateToWidth(line, width, "...");
288
+ const statusWidth = visibleWidth(status);
289
+ if (statusWidth >= width) return truncateToWidth(status, width, "");
290
+ return `${truncateToWidth(line, width - statusWidth, "...")}${status}`;
291
+ }
292
+
293
+ function getOutputPad(context: QuietToolRenderContext): number {
294
+ const outputPad = context.outputPad;
295
+ if (typeof outputPad !== "number" || !Number.isFinite(outputPad)) return 1;
296
+ return Math.max(0, Math.floor(outputPad));
297
+ }
203
298
 
204
- if ((!options.isPartial || context.isError) && state.interval) {
205
- clearInterval(state.interval);
206
- state.interval = undefined;
207
- }
299
+ function finishBashTiming(
300
+ options: ToolRenderResultParams[1],
301
+ context: QuietToolRenderContext,
302
+ ): void {
303
+ const state = context.state as BashRenderState;
304
+
305
+ if (!options.isPartial || context.isError) {
306
+ state.endedAt ??= Date.now();
307
+ if (state.interval) {
308
+ clearInterval(state.interval);
309
+ state.interval = undefined;
310
+ }
311
+ }
208
312
  }
209
313
 
210
314
  function renderQuietCollapsedResult(
211
- _result: ToolRenderResultParams[0],
212
- options: ToolRenderResultParams[1],
213
- _theme: RenderTheme,
214
- context: ToolRenderContext,
315
+ toolName: string,
316
+ _result: ToolRenderResultParams[0],
317
+ options: ToolRenderResultParams[1],
318
+ _theme: RenderTheme,
319
+ context: QuietToolRenderContext,
215
320
  ): QuietResultRenderComponent {
216
- markToolTiming(options, context);
217
- const component = context.lastComponent instanceof QuietResultRenderComponent
218
- ? context.lastComponent
219
- : new QuietResultRenderComponent();
220
- component.setLinesRenderer(() => []);
221
- return component;
222
- }
223
-
224
- function renderQuietCall(
225
- toolName: string,
226
- base: ToolDefinition,
227
- args: ToolRenderCallParams[0],
228
- theme: ToolRenderCallParams[1],
229
- context: ToolRenderCallParams[2],
230
- ) {
231
- const state = context.state as TimerRenderState;
232
- if (context.executionStarted && state.startedAt === undefined) {
233
- state.startedAt = Date.now();
234
- state.endedAt = undefined;
235
- }
236
-
237
- if (context.expanded || !QUIET_CALL_TOOL_NAMES.has(toolName)) {
238
- const delegateContext = context.lastComponent instanceof QuietCallRenderComponent
239
- ? { ...context, lastComponent: undefined }
240
- : context;
241
- return base.renderCall?.(args, theme, delegateContext) ?? new Text(theme.fg("toolTitle", theme.bold(toolName)), 0, 0);
242
- }
243
-
244
- const component = context.lastComponent instanceof QuietCallRenderComponent
245
- ? context.lastComponent
246
- : new QuietCallRenderComponent();
247
- const line = formatQuietCallLine(toolName, args, theme);
248
- const hint = formatExpandHint(theme);
249
- component.setLinesRenderer((width) => [
250
- truncateToWidth(line, width, "..."),
251
- truncateToWidth(hint, width, "..."),
252
- ]);
253
- return component;
254
- }
255
-
256
- function createQuietToolDefinition(base: ToolDefinition): ToolDefinition {
257
- const baseRenderResult = base.renderResult;
258
-
259
- return {
260
- ...base,
261
- renderCall(args, theme, context) {
262
- return renderQuietCall(base.name, base, args, theme, context);
263
- },
264
- renderResult(result, options, theme, context) {
265
- if (options.expanded && baseRenderResult) {
266
- const delegateContext = context.lastComponent instanceof QuietResultRenderComponent
267
- ? { ...context, lastComponent: undefined }
268
- : context;
269
- return baseRenderResult(result, options, theme, delegateContext);
270
- }
271
-
272
- return renderQuietCollapsedResult(result, options, theme, context);
273
- },
274
- };
275
- }
276
-
277
- function createBaseToolOptions(cwd: string): ToolsOptions | undefined {
278
- try {
279
- const settings = SettingsManager.create(cwd);
280
- return {
281
- read: { autoResizeImages: settings.getImageAutoResize() },
282
- bash: {
283
- commandPrefix: settings.getShellCommandPrefix(),
284
- shellPath: settings.getShellPath(),
285
- },
286
- };
287
- } catch {
288
- return undefined;
289
- }
290
- }
291
-
292
- function createBaseToolDefinitions(cwd: string): ToolDefinition[] {
293
- const options = createBaseToolOptions(cwd);
294
- return [
295
- createReadToolDefinition(cwd, options?.read),
296
- createBashToolDefinition(cwd, options?.bash),
297
- createEditToolDefinition(cwd, options?.edit),
298
- createWriteToolDefinition(cwd, options?.write),
299
- createGrepToolDefinition(cwd, options?.grep),
300
- createFindToolDefinition(cwd, options?.find),
301
- createLsToolDefinition(cwd, options?.ls),
302
- ] as ToolDefinition[];
303
- }
304
-
305
- function createQuietToolDefinitions(cwd: string, enabled: boolean): ToolDefinition[] {
306
- const baseDefinitions = createBaseToolDefinitions(cwd);
307
- return enabled ? baseDefinitions.map(createQuietToolDefinition) : baseDefinitions;
321
+ if (toolName === "bash") finishBashTiming(options, context);
322
+ const component =
323
+ context.lastComponent instanceof QuietResultRenderComponent
324
+ ? context.lastComponent
325
+ : new QuietResultRenderComponent();
326
+ component.setLinesRenderer(() => []);
327
+ return component;
328
+ }
329
+
330
+ function delegateCall(
331
+ nextRenderers: ToolRenderers | undefined,
332
+ args: ToolRenderCallParams[0],
333
+ theme: RenderTheme,
334
+ context: ToolRenderContext,
335
+ ): ReturnType<ToolRenderCall> {
336
+ if (!nextRenderers?.renderCall) throw new QuietToolsDelegateToDefault();
337
+ const delegateContext =
338
+ context.lastComponent instanceof QuietCallRenderComponent
339
+ ? { ...context, lastComponent: undefined }
340
+ : context;
341
+ return nextRenderers.renderCall(args, theme, delegateContext);
342
+ }
343
+
344
+ function delegateResult(
345
+ nextRenderers: ToolRenderers | undefined,
346
+ result: ToolRenderResultParams[0],
347
+ options: ToolRenderResultParams[1],
348
+ theme: RenderTheme,
349
+ context: ToolRenderContext,
350
+ ): ReturnType<ToolRenderResult> {
351
+ if (!nextRenderers?.renderResult) throw new QuietToolsDelegateToDefault();
352
+ const delegateContext =
353
+ context.lastComponent instanceof QuietResultRenderComponent
354
+ ? { ...context, lastComponent: undefined }
355
+ : context;
356
+ return nextRenderers.renderResult(result, options, theme, delegateContext);
357
+ }
358
+
359
+ export function createQuietResolver(): ToolRendererResolver {
360
+ return (toolName, next) => {
361
+ const isBuiltin = QUIET_TOOL_NAMES.has(toolName);
362
+ const isMcp = toolName.startsWith("mcp__");
363
+
364
+ if (!isBuiltin && !isMcp) return next();
365
+
366
+ const nextRenderers = next();
367
+ const usesNativeShell = nextRenderers?.renderShell === "self";
368
+
369
+ return {
370
+ renderShell: nextRenderers?.renderShell,
371
+ renderCall(args, theme, context) {
372
+ if (context.expanded) return delegateCall(nextRenderers, args, theme, context);
373
+
374
+ const quietContext = context as QuietToolRenderContext;
375
+ if (toolName === "bash") {
376
+ const state = quietContext.state as BashRenderState;
377
+ if (quietContext.executionStarted && state.startedAt === undefined) {
378
+ state.startedAt = Date.now();
379
+ state.endedAt = undefined;
380
+ }
381
+ }
382
+
383
+ const component =
384
+ context.lastComponent instanceof QuietCallRenderComponent
385
+ ? context.lastComponent
386
+ : new QuietCallRenderComponent();
387
+ if (usesNativeShell) component.setNativeFrame(theme, quietContext);
388
+ else component.clearNativeFrame();
389
+
390
+ const line = isBuiltin
391
+ ? formatQuietCallLine(toolName, args, theme)
392
+ : formatMcpCallLine(toolName, args, theme);
393
+ const status = quietContext.isError ? formatFailedMarker(theme) : "";
394
+ const hint = formatExpandHint(theme);
395
+ component.setLinesRenderer((width) => [
396
+ truncateQuietCallLine(line, status, width),
397
+ truncateToWidth(hint, width, "..."),
398
+ ]);
399
+ return component;
400
+ },
401
+ renderResult(result, options, theme, context) {
402
+ if (options.expanded) return delegateResult(nextRenderers, result, options, theme, context);
403
+ return renderQuietCollapsedResult(
404
+ toolName,
405
+ result,
406
+ options,
407
+ theme,
408
+ context as QuietToolRenderContext,
409
+ );
410
+ },
411
+ };
412
+ };
308
413
  }
309
414
 
310
415
  export const __testing = {
311
- sanitizeInlineText,
312
- formatQuietCallLine,
313
- createQuietToolDefinition,
416
+ sanitizeInlineText,
417
+ formatQuietCallLine,
418
+ formatMcpCallLine,
419
+ createQuietResolver,
420
+ QuietCallRenderComponent,
421
+ QuietResultRenderComponent,
422
+ QuietToolsDelegateToDefault,
314
423
  };
315
424
 
425
+ const STATUS_MESSAGE =
426
+ "Quiet tool previews are always on. Collapsed built-in and MCP tool rows show a one-line invocation and hide output until expanded. Model-visible tool results are unchanged.";
427
+ const FALLBACK_STATUS_MESSAGE =
428
+ "Quiet tool previews are inactive: this Pi host lacks registerToolRenderer (requires Pi >=1.0.1).";
429
+ const STATUS_USAGE = "Usage: /quiet-tools status";
430
+
316
431
  export default function quietToolsExtension(pi: ExtensionAPI) {
317
- let enabled = true;
318
-
319
- function registerTools(cwd: string): void {
320
- for (const tool of createQuietToolDefinitions(cwd, enabled)) {
321
- pi.registerTool(tool);
322
- }
323
- }
324
-
325
- pi.on("session_start", async (_event, ctx) => {
326
- registerTools(ctx.cwd);
327
- });
328
-
329
- pi.registerCommand("quiet-tools", {
330
- description: "Toggle one-line collapsed invocations for built-in tool rows",
331
- getArgumentCompletions: (prefix) => {
332
- const commands = ["on", "off", "toggle", "status"];
333
- const query = prefix.trim().toLowerCase();
334
- const matches = commands.filter((command) => command.startsWith(query));
335
- return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
336
- },
337
- handler: async (args, ctx) => {
338
- const action = args.trim().toLowerCase() || "toggle";
339
-
340
- if (action === "on" || action === "enable") {
341
- enabled = true;
342
- registerTools(ctx.cwd);
343
- ctx.ui.notify("Quiet tool previews enabled: collapsed built-in tool rows show a one-line invocation plus an expand hint.", "info");
344
- return;
345
- }
346
-
347
- if (action === "off" || action === "disable") {
348
- enabled = false;
349
- registerTools(ctx.cwd);
350
- ctx.ui.notify("Quiet tool previews disabled: restored pi's standard built-in tool renderers.", "info");
351
- return;
352
- }
353
-
354
- if (action === "toggle") {
355
- enabled = !enabled;
356
- registerTools(ctx.cwd);
357
- ctx.ui.notify(
358
- enabled
359
- ? "Quiet tool previews enabled: collapsed built-in tool rows show a one-line invocation plus an expand hint."
360
- : "Quiet tool previews disabled: restored pi's standard built-in tool renderers.",
361
- "info",
362
- );
363
- return;
364
- }
365
-
366
- if (action === "status") {
367
- ctx.ui.notify(
368
- `Quiet tool previews are ${enabled ? "enabled" : "disabled"}. Collapsed tool rows ${enabled ? "show a one-line invocation and hide output until expanded" : "use pi's default rendering"}. Model-visible tool results are unchanged.`,
369
- "info",
370
- );
371
- return;
372
- }
373
-
374
- ctx.ui.notify("Usage: /quiet-tools on | off | toggle | status", "warning");
375
- },
376
- });
432
+ let notifiedFallback = false;
433
+
434
+ const hasRenderer = typeof (pi as any).registerToolRenderer === "function";
435
+
436
+ if (hasRenderer) pi.registerToolRenderer(createQuietResolver());
437
+
438
+ pi.on("session_start", async (_event, ctx) => {
439
+ if (!hasRenderer && !notifiedFallback) {
440
+ notifiedFallback = true;
441
+ ctx.ui.notify(
442
+ "quiet-tools needs Pi >=1.0.1 to customise tool renderers. Quiet rendering is inactive.",
443
+ "warning",
444
+ );
445
+ }
446
+ });
447
+
448
+ pi.registerCommand("quiet-tools", {
449
+ description: "Show always-on one-line collapsed invocations for built-in and MCP tool rows",
450
+ getArgumentCompletions: (prefix) => {
451
+ const query = prefix.trim().toLowerCase();
452
+ return "status".startsWith(query) ? [{ value: "status", label: "status" }] : null;
453
+ },
454
+ handler: async (args, ctx) => {
455
+ if (!args.trim() || args.trim().toLowerCase() === "status") {
456
+ ctx.ui.notify(hasRenderer ? STATUS_MESSAGE : FALLBACK_STATUS_MESSAGE, "info");
457
+ return;
458
+ }
459
+ ctx.ui.notify(STATUS_USAGE, "warning");
460
+ },
461
+ });
377
462
  }