@diegopetrucci/pi-extensions 0.1.19 → 0.1.21
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 +3 -2
- package/extensions/openai-fast/README.md +95 -0
- package/extensions/openai-fast/index.ts +292 -0
- package/extensions/openai-fast/openai-fast.example.json +4 -0
- package/extensions/openai-fast/package.json +28 -0
- package/extensions/quiet-tools/README.md +3 -2
- package/extensions/quiet-tools/index.ts +169 -182
- package/extensions/quiet-tools/package.json +2 -2
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -8,9 +8,10 @@ A collection of [pi](https://github.com/earendil-works/pi-mono) agent extensions
|
|
|
8
8
|
- [`librarian`](./extensions/librarian): Adds a GitHub research scout that asks whether to use an opt-in local repo checkout cache under the OS user cache directory, with cached repos expiring after 30 days of non-use.
|
|
9
9
|
- [`minimal-footer`](./extensions/minimal-footer): Replaces pi's built-in footer with a minimal configurable two-line layout: branch/repo on the first line, context/model on the second, optional `DUMB ZONE`, plus OpenAI Codex 5-hour and 7-day usage when available.
|
|
10
10
|
- [`notify`](./extensions/notify): Sends configurable terminal, desktop, bell, and sound notifications when pi finishes and is ready for input.
|
|
11
|
+
- [`openai-fast`](./extensions/openai-fast): Adds `/fast` to enable OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4 and GPT-5.5 by injecting the priority service tier.
|
|
11
12
|
- [`oracle`](./extensions/oracle): Adds an Amp-style read-only oracle tool that auto-selects the strongest reasoning model on the current provider/subscription, covers pi’s built-in providers with hardcoded rankings, sets reasoning to xhigh by default, and shows live status while running.
|
|
12
13
|
- [`permission-gate`](./extensions/permission-gate): Prompts for confirmation before dangerous bash commands like `rm -rf`, `sudo`, and `chmod 777`.
|
|
13
|
-
- [`quiet-tools`](./extensions/quiet-tools): Renders collapsed built-in tool rows as
|
|
14
|
+
- [`quiet-tools`](./extensions/quiet-tools): Renders collapsed built-in tool rows as a one-line invocation plus an expand hint without changing model-visible tool results; toggle temporarily with `/quiet-tools`.
|
|
14
15
|
|
|
15
16
|
(For the full list of pi extensions I use, [check out my dotfiles](https://github.com/diegopetrucci/dot/blob/main/.pi/agent/settings.json).)
|
|
16
17
|
|
|
@@ -25,7 +26,7 @@ pi install npm:@diegopetrucci/pi-extensions
|
|
|
25
26
|
Or pin the GitHub package to this release:
|
|
26
27
|
|
|
27
28
|
```bash
|
|
28
|
-
pi install git:github.com/diegopetrucci/pi-extensions@v0.1.
|
|
29
|
+
pi install git:github.com/diegopetrucci/pi-extensions@v0.1.21
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
Or a specific extension:
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# openai-fast
|
|
2
|
+
|
|
3
|
+
A pi extension that enables OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4 and GPT-5.5.
|
|
4
|
+
|
|
5
|
+
When active, the extension injects this into eligible OpenAI Codex request payloads:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"service_tier": "priority"
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The user-facing feature is OpenAI Codex **Fast mode**. The wire value is `priority` because current Codex clients map Fast mode to the OpenAI priority service tier.
|
|
14
|
+
|
|
15
|
+
## Eligibility
|
|
16
|
+
|
|
17
|
+
Fast mode is only injected when all of these are true:
|
|
18
|
+
|
|
19
|
+
- The current provider is `openai-codex`.
|
|
20
|
+
- The current API is `openai-codex-responses`.
|
|
21
|
+
- The current model is `gpt-5.4` or `gpt-5.5`.
|
|
22
|
+
- The provider is using ChatGPT OAuth/subscription auth, not API-key auth.
|
|
23
|
+
- The request payload does not already include `service_tier`.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
/fast status
|
|
29
|
+
/fast on
|
|
30
|
+
/fast off
|
|
31
|
+
/fast auto
|
|
32
|
+
/fast toggle
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`/fast on` and `/fast off` are temporary session/runtime overrides. Use `/fast auto` to reload and follow config defaults again.
|
|
36
|
+
|
|
37
|
+
The extension defaults to off so installing the full collection does not accidentally spend Fast-mode credits.
|
|
38
|
+
|
|
39
|
+
## Config
|
|
40
|
+
|
|
41
|
+
Optional global config:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
~/.pi/agent/extensions/openai-fast.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Optional project config:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
.pi/openai-fast.json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Project config overrides global config.
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"enabled": false,
|
|
58
|
+
"showStatus": true
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- `enabled`: default Fast-mode state when there is no session override.
|
|
63
|
+
- `showStatus`: show a compact `fast` status when Fast mode is active for the current model.
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
### Standalone npm package
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pi install npm:@diegopetrucci/pi-openai-fast
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Collection package
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pi install npm:@diegopetrucci/pi-extensions
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### GitHub package
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pi install git:github.com/diegopetrucci/pi-extensions
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Then reload pi:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
/reload
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Notes
|
|
92
|
+
|
|
93
|
+
- This extension intentionally does not affect API-key OpenAI models.
|
|
94
|
+
- Pi may only account Fast-mode cost correctly when the backend reports `service_tier: "priority"` in the streamed response. The extension does not patch usage totals to avoid double-counting.
|
|
95
|
+
- If pi adds first-class service-tier support later, this extension skips payloads that already contain `service_tier`.
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
getAgentDir,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionContext,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
const EXTENSION_ID = "openai-fast";
|
|
10
|
+
const PROVIDER_ID = "openai-codex";
|
|
11
|
+
const API_ID = "openai-codex-responses";
|
|
12
|
+
const FAST_SERVICE_TIER = "priority";
|
|
13
|
+
const SUPPORTED_MODELS = new Set(["gpt-5.4", "gpt-5.5"]);
|
|
14
|
+
const COMMANDS = ["status", "on", "off", "auto", "toggle"];
|
|
15
|
+
|
|
16
|
+
const DEFAULT_CONFIG: OpenAIFastConfig = {
|
|
17
|
+
enabled: false,
|
|
18
|
+
showStatus: true,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type FastOverride = "auto" | "on" | "off";
|
|
22
|
+
|
|
23
|
+
type OpenAIFastConfig = {
|
|
24
|
+
/** Default Fast-mode state when there is no session override. */
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
/** Show a compact `fast` status when Fast mode is active for the current model. */
|
|
27
|
+
showStatus: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type SessionState = {
|
|
31
|
+
config: OpenAIFastConfig;
|
|
32
|
+
override: FastOverride;
|
|
33
|
+
lastInjectedAt?: number;
|
|
34
|
+
lastInjectedModel?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type RecursivePartial<T> = {
|
|
38
|
+
[P in keyof T]?: T[P] extends object ? RecursivePartial<T[P]> : T[P];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type PayloadRecord = Record<string, unknown>;
|
|
42
|
+
|
|
43
|
+
type Eligibility = {
|
|
44
|
+
eligible: boolean;
|
|
45
|
+
modelKey: string;
|
|
46
|
+
reason?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function readConfigFile(path: string): RecursivePartial<OpenAIFastConfig> {
|
|
50
|
+
if (!existsSync(path)) return {};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
54
|
+
return isPayloadRecord(parsed) ? (parsed as RecursivePartial<OpenAIFastConfig>) : {};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(`Warning: Could not parse ${path}: ${error}`);
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeBoolean(value: unknown, fallback: boolean): boolean {
|
|
62
|
+
return typeof value === "boolean" ? value : fallback;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mergeConfig(
|
|
66
|
+
base: OpenAIFastConfig,
|
|
67
|
+
overrides: RecursivePartial<OpenAIFastConfig>,
|
|
68
|
+
): OpenAIFastConfig {
|
|
69
|
+
return {
|
|
70
|
+
enabled: normalizeBoolean(overrides.enabled, base.enabled),
|
|
71
|
+
showStatus: normalizeBoolean(overrides.showStatus, base.showStatus),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function findProjectConfigPath(cwd: string): string {
|
|
76
|
+
let current = cwd;
|
|
77
|
+
while (true) {
|
|
78
|
+
const candidate = join(current, ".pi", "openai-fast.json");
|
|
79
|
+
if (existsSync(candidate)) return candidate;
|
|
80
|
+
|
|
81
|
+
const parent = dirname(current);
|
|
82
|
+
if (parent === current) return join(cwd, ".pi", "openai-fast.json");
|
|
83
|
+
current = parent;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function loadConfig(cwd: string): OpenAIFastConfig {
|
|
88
|
+
const globalConfig = readConfigFile(join(getAgentDir(), "extensions", "openai-fast.json"));
|
|
89
|
+
const projectConfig = readConfigFile(findProjectConfigPath(cwd));
|
|
90
|
+
return mergeConfig(mergeConfig(DEFAULT_CONFIG, globalConfig), projectConfig);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isPayloadRecord(payload: unknown): payload is PayloadRecord {
|
|
94
|
+
return typeof payload === "object" && payload !== null && !Array.isArray(payload);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function modelKey(ctx: ExtensionContext): string {
|
|
98
|
+
const model = ctx.model;
|
|
99
|
+
return model ? `${model.provider}/${model.id}` : "no-model";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isFastEnabled(state: SessionState): boolean {
|
|
103
|
+
if (state.override === "on") return true;
|
|
104
|
+
if (state.override === "off") return false;
|
|
105
|
+
return state.config.enabled;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function describeMode(state: SessionState): string {
|
|
109
|
+
if (state.override === "on") return "on (session override)";
|
|
110
|
+
if (state.override === "off") return "off (session override)";
|
|
111
|
+
return state.config.enabled ? "on (config default)" : "off (config default)";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getEligibility(ctx: ExtensionContext): Eligibility {
|
|
115
|
+
const model = ctx.model;
|
|
116
|
+
if (!model) {
|
|
117
|
+
return { eligible: false, modelKey: "no-model", reason: "no model is selected" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const key = `${model.provider}/${model.id}`;
|
|
121
|
+
if (model.provider !== PROVIDER_ID) {
|
|
122
|
+
return {
|
|
123
|
+
eligible: false,
|
|
124
|
+
modelKey: key,
|
|
125
|
+
reason: `current provider is ${model.provider}, not ${PROVIDER_ID}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (model.api !== API_ID) {
|
|
130
|
+
return {
|
|
131
|
+
eligible: false,
|
|
132
|
+
modelKey: key,
|
|
133
|
+
reason: `current API is ${model.api}, not ${API_ID}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!SUPPORTED_MODELS.has(model.id)) {
|
|
138
|
+
return {
|
|
139
|
+
eligible: false,
|
|
140
|
+
modelKey: key,
|
|
141
|
+
reason: "Fast mode is only enabled for gpt-5.4 and gpt-5.5",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!ctx.modelRegistry.isUsingOAuth(model)) {
|
|
146
|
+
return {
|
|
147
|
+
eligible: false,
|
|
148
|
+
modelKey: key,
|
|
149
|
+
reason: "ChatGPT OAuth auth is required; API-key auth is intentionally not used",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { eligible: true, modelKey: key };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function updateStatus(ctx: ExtensionContext, state: SessionState): void {
|
|
157
|
+
if (!ctx.hasUI) return;
|
|
158
|
+
if (!state.config.showStatus) {
|
|
159
|
+
ctx.ui.setStatus(EXTENSION_ID, undefined);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const eligibility = getEligibility(ctx);
|
|
164
|
+
ctx.ui.setStatus(
|
|
165
|
+
EXTENSION_ID,
|
|
166
|
+
isFastEnabled(state) && eligibility.eligible ? "fast" : undefined,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getStatusMessage(ctx: ExtensionContext, state: SessionState): string {
|
|
171
|
+
const enabled = isFastEnabled(state);
|
|
172
|
+
const eligibility = getEligibility(ctx);
|
|
173
|
+
const active = enabled && eligibility.eligible;
|
|
174
|
+
const injected = state.lastInjectedAt
|
|
175
|
+
? ` Last injected for ${state.lastInjectedModel ?? "unknown model"} ${Math.max(0, Math.round((Date.now() - state.lastInjectedAt) / 1000))}s ago.`
|
|
176
|
+
: "";
|
|
177
|
+
|
|
178
|
+
if (active) {
|
|
179
|
+
return `OpenAI Fast mode is ${describeMode(state)} and active for ${eligibility.modelKey}; requests will use service_tier=${FAST_SERVICE_TIER}.${injected}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (enabled) {
|
|
183
|
+
return `OpenAI Fast mode is ${describeMode(state)}, but inactive for ${eligibility.modelKey}: ${eligibility.reason}.${injected}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return `OpenAI Fast mode is ${describeMode(state)}. Current model: ${eligibility.modelKey}.${injected}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function injectFastServiceTier(
|
|
190
|
+
payload: unknown,
|
|
191
|
+
ctx: ExtensionContext,
|
|
192
|
+
state: SessionState,
|
|
193
|
+
): PayloadRecord | undefined {
|
|
194
|
+
if (!isFastEnabled(state)) return undefined;
|
|
195
|
+
if (!getEligibility(ctx).eligible) return undefined;
|
|
196
|
+
if (!isPayloadRecord(payload)) return undefined;
|
|
197
|
+
if (payload.model !== ctx.model?.id) return undefined;
|
|
198
|
+
if ("service_tier" in payload) return undefined;
|
|
199
|
+
|
|
200
|
+
state.lastInjectedAt = Date.now();
|
|
201
|
+
state.lastInjectedModel = modelKey(ctx);
|
|
202
|
+
return {
|
|
203
|
+
...payload,
|
|
204
|
+
service_tier: FAST_SERVICE_TIER,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export default function openAIFastExtension(pi: ExtensionAPI) {
|
|
209
|
+
const states = new WeakMap<object, SessionState>();
|
|
210
|
+
|
|
211
|
+
function getState(ctx: ExtensionContext): SessionState {
|
|
212
|
+
let state = states.get(ctx.sessionManager);
|
|
213
|
+
if (!state) {
|
|
214
|
+
state = {
|
|
215
|
+
config: loadConfig(ctx.cwd),
|
|
216
|
+
override: "auto",
|
|
217
|
+
};
|
|
218
|
+
states.set(ctx.sessionManager, state);
|
|
219
|
+
}
|
|
220
|
+
return state;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
pi.on("session_start", (_event, ctx) => {
|
|
224
|
+
const state: SessionState = {
|
|
225
|
+
config: loadConfig(ctx.cwd),
|
|
226
|
+
override: "auto",
|
|
227
|
+
};
|
|
228
|
+
states.set(ctx.sessionManager, state);
|
|
229
|
+
updateStatus(ctx, state);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
pi.on("model_select", (_event, ctx) => {
|
|
233
|
+
updateStatus(ctx, getState(ctx));
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
237
|
+
const state = getState(ctx);
|
|
238
|
+
const nextPayload = injectFastServiceTier(event.payload, ctx, state);
|
|
239
|
+
updateStatus(ctx, state);
|
|
240
|
+
return nextPayload;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
pi.registerCommand("fast", {
|
|
244
|
+
description: "Manage OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4/GPT-5.5",
|
|
245
|
+
getArgumentCompletions: (prefix) => {
|
|
246
|
+
const normalized = prefix.trim().toLowerCase();
|
|
247
|
+
const matches = COMMANDS.filter((command) => command.startsWith(normalized));
|
|
248
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
249
|
+
},
|
|
250
|
+
handler: async (args, ctx) => {
|
|
251
|
+
const state = getState(ctx);
|
|
252
|
+
const action = args.trim().toLowerCase() || "status";
|
|
253
|
+
|
|
254
|
+
if (action === "on" || action === "enable") {
|
|
255
|
+
state.override = "on";
|
|
256
|
+
updateStatus(ctx, state);
|
|
257
|
+
ctx.ui.notify(getStatusMessage(ctx, state), "info");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (action === "off" || action === "disable") {
|
|
262
|
+
state.override = "off";
|
|
263
|
+
updateStatus(ctx, state);
|
|
264
|
+
ctx.ui.notify(getStatusMessage(ctx, state), "info");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (action === "auto" || action === "default") {
|
|
269
|
+
state.override = "auto";
|
|
270
|
+
state.config = loadConfig(ctx.cwd);
|
|
271
|
+
updateStatus(ctx, state);
|
|
272
|
+
ctx.ui.notify(getStatusMessage(ctx, state), "info");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (action === "toggle") {
|
|
277
|
+
state.override = isFastEnabled(state) ? "off" : "on";
|
|
278
|
+
updateStatus(ctx, state);
|
|
279
|
+
ctx.ui.notify(getStatusMessage(ctx, state), "info");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (action === "status") {
|
|
284
|
+
updateStatus(ctx, state);
|
|
285
|
+
ctx.ui.notify(getStatusMessage(ctx, state), "info");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
ctx.ui.notify("Usage: /fast on | off | auto | toggle | status", "warning");
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@diegopetrucci/pi-openai-fast",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A pi extension that enables OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4 and GPT-5.5 by injecting the priority service tier.",
|
|
5
|
+
"keywords": ["pi-package", "pi", "openai", "codex", "fast"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/diegopetrucci/pi-extensions.git",
|
|
10
|
+
"directory": "extensions/openai-fast"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.ts",
|
|
14
|
+
"openai-fast.example.json",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"pi": {
|
|
21
|
+
"extensions": [
|
|
22
|
+
"index.ts"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A pi extension that makes collapsed built-in tool rows much quieter in the TUI.
|
|
4
4
|
|
|
5
|
-
When enabled, collapsed tool
|
|
5
|
+
When enabled, each collapsed tool row renders as one invocation line plus a separate `(Ctrl+O to expand)` hint line. Tool output is hidden until expanded. Expanding with `Ctrl+O` still shows pi's full rendered output.
|
|
6
6
|
|
|
7
7
|
`quiet-tools` only changes the visual renderer. It does not truncate, summarize, or rewrite the actual tool results sent to the model.
|
|
8
8
|
|
|
@@ -16,7 +16,7 @@ When enabled, collapsed tool output renders as one output line plus an inline hi
|
|
|
16
16
|
- `edit`
|
|
17
17
|
- `write`
|
|
18
18
|
|
|
19
|
-
For
|
|
19
|
+
For every covered tool, the collapsed invocation is truncated to a single visual line so long paths, commands, diffs, or file contents do not fill the TUI. Expanding restores pi's normal renderer.
|
|
20
20
|
|
|
21
21
|
## Commands
|
|
22
22
|
|
|
@@ -61,3 +61,4 @@ Then reload pi:
|
|
|
61
61
|
- It reuses pi's built-in implementations and preserves `shellPath`, `shellCommandPrefix`, and image autoresize settings when they are available from settings files.
|
|
62
62
|
- If another extension also overrides built-in tool execution, pi's extension load order determines which override wins.
|
|
63
63
|
- It affects assistant-invoked tool rows. User `!`/`!!` bash commands are rendered by a separate pi component and keep pi's default preview behavior.
|
|
64
|
+
- Pi renders image attachments outside tool result renderers, so inline image display for image reads is still controlled by pi's image settings.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
createBashToolDefinition,
|
|
3
5
|
createEditToolDefinition,
|
|
@@ -6,19 +8,15 @@ import {
|
|
|
6
8
|
createLsToolDefinition,
|
|
7
9
|
createReadToolDefinition,
|
|
8
10
|
createWriteToolDefinition,
|
|
9
|
-
DEFAULT_MAX_BYTES,
|
|
10
|
-
DEFAULT_MAX_LINES,
|
|
11
|
-
formatSize,
|
|
12
11
|
keyHint,
|
|
13
12
|
SettingsManager,
|
|
14
13
|
type ExtensionAPI,
|
|
15
14
|
type ToolDefinition as PiToolDefinition,
|
|
16
15
|
type ToolsOptions,
|
|
17
16
|
} from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import {
|
|
17
|
+
import { Container, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
19
18
|
|
|
20
|
-
const
|
|
21
|
-
const QUIET_CALL_TOOL_NAMES = new Set(["edit", "write"]);
|
|
19
|
+
const QUIET_CALL_TOOL_NAMES = new Set(["bash", "edit", "find", "grep", "ls", "read", "write"]);
|
|
22
20
|
|
|
23
21
|
type ToolDefinition = PiToolDefinition<any, any, any>;
|
|
24
22
|
type ToolRenderCall = NonNullable<ToolDefinition["renderCall"]>;
|
|
@@ -34,208 +32,195 @@ type TimerRenderState = {
|
|
|
34
32
|
interval?: ReturnType<typeof setInterval>;
|
|
35
33
|
};
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
35
|
+
class QuietLinesRenderComponent extends Container {
|
|
36
|
+
private linesRenderer: ((width: number) => string[]) | undefined;
|
|
37
|
+
|
|
38
|
+
setLinesRenderer(linesRenderer: (width: number) => string[]): void {
|
|
39
|
+
this.linesRenderer = linesRenderer;
|
|
40
|
+
this.invalidate();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
render(width: number): string[] {
|
|
44
|
+
return this.linesRenderer?.(width).filter((line) => line) ?? [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
46
47
|
|
|
47
|
-
class
|
|
48
|
+
class QuietCallRenderComponent extends QuietLinesRenderComponent {}
|
|
49
|
+
class QuietResultRenderComponent extends QuietLinesRenderComponent {}
|
|
48
50
|
|
|
49
|
-
function
|
|
51
|
+
function sanitizeInlineText(text: string): string {
|
|
50
52
|
return text
|
|
51
53
|
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
52
54
|
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
55
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
53
56
|
.replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "");
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
function
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.map((content) => content.text ?? "")
|
|
61
|
-
.join("\n")
|
|
62
|
-
.trim(),
|
|
63
|
-
);
|
|
59
|
+
function str(value: unknown): string | null {
|
|
60
|
+
if (typeof value === "string") return sanitizeInlineText(value);
|
|
61
|
+
if (value == null) return "";
|
|
62
|
+
return null;
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
function
|
|
67
|
-
return
|
|
65
|
+
function asRecord(args: unknown): Record<string, unknown> | undefined {
|
|
66
|
+
return args && typeof args === "object" ? (args as Record<string, unknown>) : undefined;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
function
|
|
71
|
-
|
|
69
|
+
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 "";
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
function
|
|
75
|
-
|
|
80
|
+
function numberArg(args: unknown, name: string): number | undefined {
|
|
81
|
+
const value = asRecord(args)?.[name];
|
|
82
|
+
return typeof value === "number" ? value : undefined;
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const styledLine = theme.fg("toolOutput", line);
|
|
82
|
-
if (hiddenLines <= 0) {
|
|
83
|
-
return truncateToWidth(styledLine, width, "...");
|
|
84
|
-
}
|
|
85
|
+
function invalidArgText(theme: RenderTheme): string {
|
|
86
|
+
return theme.fg("error", "[invalid arg]");
|
|
87
|
+
}
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const hintWidth = visibleLength(hint);
|
|
91
|
-
if (hintWidth + 8 > width) {
|
|
92
|
-
return truncateToWidth(`${styledLine}${hint}`, width, "...");
|
|
93
|
-
}
|
|
89
|
+
function shortenPath(path: string): string {
|
|
90
|
+
const home = homedir();
|
|
91
|
+
return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
92
|
+
}
|
|
94
93
|
|
|
95
|
-
|
|
94
|
+
function formatToolTitle(toolName: string, theme: RenderTheme): string {
|
|
95
|
+
return theme.fg("toolTitle", theme.bold(toolName));
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
function
|
|
99
|
-
|
|
98
|
+
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);
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
function
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
linesTruncated?: boolean;
|
|
111
|
-
}
|
|
112
|
-
| undefined;
|
|
113
|
-
const truncation = details?.truncation;
|
|
114
|
-
const warnings: string[] = [];
|
|
115
|
-
|
|
116
|
-
if (details?.fullOutputPath) {
|
|
117
|
-
warnings.push(`Full output: ${details.fullOutputPath}`);
|
|
118
|
-
}
|
|
104
|
+
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}` : ""}`);
|
|
111
|
+
}
|
|
119
112
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
} else if (truncation.truncatedBy === "lines") {
|
|
124
|
-
warnings.push(
|
|
125
|
-
`Truncated: showing ${truncation.outputLines ?? "some"} of ${truncation.totalLines ?? "?"} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)`,
|
|
126
|
-
);
|
|
127
|
-
} else {
|
|
128
|
-
warnings.push(
|
|
129
|
-
`Truncated: ${truncation.outputLines ?? "some"} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
113
|
+
function formatQuietReadCall(args: unknown, theme: RenderTheme): string {
|
|
114
|
+
return `${formatToolTitle("read", theme)} ${formatPathArg(args, theme)}${formatReadLineRange(args, theme)}`;
|
|
115
|
+
}
|
|
133
116
|
|
|
134
|
-
|
|
135
|
-
|
|
117
|
+
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}`;
|
|
127
|
+
}
|
|
136
128
|
|
|
137
|
-
|
|
138
|
-
|
|
129
|
+
function formatSearchPath(rawPath: string | null, theme: RenderTheme): string {
|
|
130
|
+
return rawPath === null ? invalidArgText(theme) : shortenPath(rawPath || ".");
|
|
131
|
+
}
|
|
139
132
|
|
|
140
|
-
|
|
141
|
-
|
|
133
|
+
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;
|
|
145
|
+
}
|
|
142
146
|
|
|
143
|
-
|
|
147
|
+
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;
|
|
156
|
+
}
|
|
144
157
|
|
|
145
|
-
|
|
158
|
+
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;
|
|
146
166
|
}
|
|
147
167
|
|
|
148
|
-
function
|
|
149
|
-
|
|
168
|
+
function formatQuietPathOnlyCall(toolName: "edit" | "write", args: unknown, theme: RenderTheme): string {
|
|
169
|
+
return `${formatToolTitle(toolName, theme)} ${formatPathArg(args, theme)}`;
|
|
170
|
+
}
|
|
150
171
|
|
|
151
|
-
|
|
152
|
-
|
|
172
|
+
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);
|
|
153
190
|
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function formatExpandHint(theme: RenderTheme): string {
|
|
194
|
+
return `${theme.fg("muted", "(")}${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
|
|
195
|
+
}
|
|
154
196
|
|
|
155
|
-
|
|
197
|
+
function markToolTiming(options: ToolRenderResultParams[1], context: ToolRenderContext): void {
|
|
198
|
+
const state = context.state as TimerRenderState;
|
|
199
|
+
|
|
200
|
+
if ((!options.isPartial || context.isError) && state.startedAt !== undefined) {
|
|
156
201
|
state.endedAt ??= Date.now();
|
|
157
|
-
if (state.interval) {
|
|
158
|
-
clearInterval(state.interval);
|
|
159
|
-
state.interval = undefined;
|
|
160
|
-
}
|
|
161
202
|
}
|
|
162
203
|
|
|
163
|
-
|
|
204
|
+
if ((!options.isPartial || context.isError) && state.interval) {
|
|
205
|
+
clearInterval(state.interval);
|
|
206
|
+
state.interval = undefined;
|
|
207
|
+
}
|
|
164
208
|
}
|
|
165
209
|
|
|
166
210
|
function renderQuietCollapsedResult(
|
|
167
|
-
|
|
211
|
+
_result: ToolRenderResultParams[0],
|
|
168
212
|
options: ToolRenderResultParams[1],
|
|
169
|
-
|
|
213
|
+
_theme: RenderTheme,
|
|
170
214
|
context: ToolRenderContext,
|
|
171
|
-
):
|
|
172
|
-
|
|
215
|
+
): QuietResultRenderComponent {
|
|
216
|
+
markToolTiming(options, context);
|
|
173
217
|
const component = context.lastComponent instanceof QuietResultRenderComponent
|
|
174
218
|
? context.lastComponent
|
|
175
219
|
: new QuietResultRenderComponent();
|
|
176
|
-
component.
|
|
177
|
-
|
|
178
|
-
const output = getTextOutput(result);
|
|
179
|
-
const outputLines = output ? output.split("\n") : [];
|
|
180
|
-
|
|
181
|
-
if (outputLines.length > 0) {
|
|
182
|
-
const firstLine = outputLines[0] ?? "";
|
|
183
|
-
const hiddenLines = Math.max(0, outputLines.length - COLLAPSED_PREVIEW_LINES);
|
|
184
|
-
component.addChild({
|
|
185
|
-
render: (width) => [renderCollapsedLine(firstLine, hiddenLines, theme, width)],
|
|
186
|
-
invalidate: () => undefined,
|
|
187
|
-
});
|
|
188
|
-
} else if (options.isPartial) {
|
|
189
|
-
component.addChild(new Text(theme.fg("muted", "Running..."), 0, 0));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const warnings = getTruncationWarnings(result);
|
|
193
|
-
if (warnings.length > 0) {
|
|
194
|
-
component.addChild(new Text(theme.fg("warning", `[${warnings.join(". ")}]`), 0, 0));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (state.startedAt !== undefined && options.isPartial) {
|
|
198
|
-
component.addChild(new Text(theme.fg("muted", `Elapsed ${formatDuration(Date.now() - state.startedAt)}`), 0, 0));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
component.invalidate();
|
|
220
|
+
component.setLinesRenderer(() => []);
|
|
202
221
|
return component;
|
|
203
222
|
}
|
|
204
223
|
|
|
205
|
-
function getPathArg(args: unknown): string | undefined {
|
|
206
|
-
if (!args || typeof args !== "object") return undefined;
|
|
207
|
-
const values = args as { path?: unknown; file_path?: unknown };
|
|
208
|
-
return typeof values.file_path === "string"
|
|
209
|
-
? values.file_path
|
|
210
|
-
: typeof values.path === "string"
|
|
211
|
-
? values.path
|
|
212
|
-
: undefined;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function formatQuietEditCall(args: unknown, theme: ToolRenderCallParams[1]): string {
|
|
216
|
-
const path = getPathArg(args);
|
|
217
|
-
const edits = args && typeof args === "object" && Array.isArray((args as { edits?: unknown }).edits)
|
|
218
|
-
? (args as { edits: unknown[] }).edits.length
|
|
219
|
-
: undefined;
|
|
220
|
-
const editSummary = typeof edits === "number" && edits > 0 ? `${plural(edits, "edit block")} hidden` : "preview hidden";
|
|
221
|
-
return `${theme.fg("toolTitle", theme.bold("edit"))} ${theme.fg("accent", path ?? "...")}${theme.fg(
|
|
222
|
-
"muted",
|
|
223
|
-
` ... ${editSummary} (${keyHint("app.tools.expand", "to expand")})`,
|
|
224
|
-
)}`;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function formatQuietWriteCall(args: unknown, theme: ToolRenderCallParams[1]): string {
|
|
228
|
-
const path = getPathArg(args);
|
|
229
|
-
const content = args && typeof args === "object" ? (args as { content?: unknown }).content : undefined;
|
|
230
|
-
const contentSummary = typeof content === "string"
|
|
231
|
-
? `${plural(content.split("\n").length, "line")}, ${formatSize(Buffer.byteLength(content, "utf8"))} hidden`
|
|
232
|
-
: "content hidden";
|
|
233
|
-
return `${theme.fg("toolTitle", theme.bold("write"))} ${theme.fg("accent", path ?? "...")}${theme.fg(
|
|
234
|
-
"muted",
|
|
235
|
-
` ... ${contentSummary} (${keyHint("app.tools.expand", "to expand")})`,
|
|
236
|
-
)}`;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
224
|
function renderQuietCall(
|
|
240
225
|
toolName: string,
|
|
241
226
|
base: ToolDefinition,
|
|
@@ -243,27 +228,29 @@ function renderQuietCall(
|
|
|
243
228
|
theme: ToolRenderCallParams[1],
|
|
244
229
|
context: ToolRenderCallParams[2],
|
|
245
230
|
) {
|
|
246
|
-
|
|
247
|
-
|
|
231
|
+
const state = context.state as TimerRenderState;
|
|
232
|
+
if (context.executionStarted && state.startedAt === undefined) {
|
|
233
|
+
state.startedAt = Date.now();
|
|
234
|
+
state.endedAt = undefined;
|
|
248
235
|
}
|
|
249
236
|
|
|
250
|
-
if (
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
? "toolSuccessBg"
|
|
256
|
-
: "toolPendingBg";
|
|
257
|
-
component.setBgFn((text) => theme.bg(bgColor, text));
|
|
258
|
-
component.clear();
|
|
259
|
-
component.addChild(new Text(formatQuietEditCall(args, theme), 0, 0));
|
|
260
|
-
component.invalidate();
|
|
261
|
-
return component;
|
|
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);
|
|
262
242
|
}
|
|
263
243
|
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
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;
|
|
267
254
|
}
|
|
268
255
|
|
|
269
256
|
function createQuietToolDefinition(base: ToolDefinition): ToolDefinition {
|
|
@@ -334,7 +321,7 @@ export default function quietToolsExtension(pi: ExtensionAPI) {
|
|
|
334
321
|
});
|
|
335
322
|
|
|
336
323
|
pi.registerCommand("quiet-tools", {
|
|
337
|
-
description: "Toggle
|
|
324
|
+
description: "Toggle one-line collapsed invocations for built-in tool rows",
|
|
338
325
|
getArgumentCompletions: (prefix) => {
|
|
339
326
|
const commands = ["on", "off", "toggle", "status"];
|
|
340
327
|
const query = prefix.trim().toLowerCase();
|
|
@@ -347,7 +334,7 @@ export default function quietToolsExtension(pi: ExtensionAPI) {
|
|
|
347
334
|
if (action === "on" || action === "enable") {
|
|
348
335
|
enabled = true;
|
|
349
336
|
registerTools(ctx.cwd);
|
|
350
|
-
ctx.ui.notify("Quiet tool previews enabled: collapsed built-in tool rows show one-line
|
|
337
|
+
ctx.ui.notify("Quiet tool previews enabled: collapsed built-in tool rows show a one-line invocation plus an expand hint.", "info");
|
|
351
338
|
return;
|
|
352
339
|
}
|
|
353
340
|
|
|
@@ -363,7 +350,7 @@ export default function quietToolsExtension(pi: ExtensionAPI) {
|
|
|
363
350
|
registerTools(ctx.cwd);
|
|
364
351
|
ctx.ui.notify(
|
|
365
352
|
enabled
|
|
366
|
-
? "Quiet tool previews enabled: collapsed built-in tool rows show one-line
|
|
353
|
+
? "Quiet tool previews enabled: collapsed built-in tool rows show a one-line invocation plus an expand hint."
|
|
367
354
|
: "Quiet tool previews disabled: restored pi's standard built-in tool renderers.",
|
|
368
355
|
"info",
|
|
369
356
|
);
|
|
@@ -372,7 +359,7 @@ export default function quietToolsExtension(pi: ExtensionAPI) {
|
|
|
372
359
|
|
|
373
360
|
if (action === "status") {
|
|
374
361
|
ctx.ui.notify(
|
|
375
|
-
`Quiet tool previews are ${enabled ? "enabled" : "disabled"}. Collapsed
|
|
362
|
+
`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.`,
|
|
376
363
|
"info",
|
|
377
364
|
);
|
|
378
365
|
return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-quiet-tools",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A pi extension that visually compacts collapsed built-in tool
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "A pi extension that visually compacts collapsed built-in tool rows in the TUI without changing tool results sent to the model.",
|
|
5
5
|
"keywords": ["pi-package", "pi", "tools", "terminal", "tui"],
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A collection of pi extensions, including a GitHub librarian with opt-in local repo checkout caching, a minimal custom footer, an Amp-style oracle, a 200k context cap for auto-compaction, a local HTML context inspector, quiet one-line collapsed
|
|
3
|
+
"version": "0.1.21",
|
|
4
|
+
"description": "A collection of pi extensions, including a GitHub librarian with opt-in local repo checkout caching, a minimal custom footer, an Amp-style oracle, a 200k context cap for auto-compaction, a local HTML context inspector, OpenAI Codex Fast mode controls, quiet one-line collapsed invocation previews, a permission gate for dangerous bash commands, confirm-before-destructive session actions, and terminal notifications when pi is ready for input.",
|
|
5
5
|
"keywords": ["pi-package", "pi", "terminal", "agent"],
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"./extensions/quiet-tools/index.ts",
|
|
37
37
|
"./extensions/permission-gate/index.ts",
|
|
38
38
|
"./extensions/confirm-destructive/index.ts",
|
|
39
|
-
"./extensions/notify/index.ts"
|
|
39
|
+
"./extensions/notify/index.ts",
|
|
40
|
+
"./extensions/openai-fast/index.ts"
|
|
40
41
|
],
|
|
41
42
|
"image": "https://raw.githubusercontent.com/diegopetrucci/pi-extensions/main/assets/oracle-preview.svg"
|
|
42
43
|
}
|