@diegopetrucci/pi-extensions 0.1.14 → 0.1.16
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 -1
- package/extensions/compact-bash/README.md +49 -0
- package/extensions/compact-bash/index.ts +241 -0
- package/extensions/compact-bash/package.json +28 -0
- package/extensions/context-cap/README.md +60 -0
- package/extensions/context-cap/index.ts +179 -0
- package/extensions/context-cap/package.json +28 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ A collection of [pi](https://github.com/earendil-works/pi-mono) agent extensions
|
|
|
4
4
|
|
|
5
5
|
- [`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.
|
|
6
6
|
- [`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.
|
|
7
|
+
- [`context-cap`](./extensions/context-cap): Caps effective model context windows at 200k tokens by default so pi avoids the `dumb zone`; toggle temporarily with `/context-cap`.
|
|
8
|
+
- [`compact-bash`](./extensions/compact-bash): Renders collapsed assistant `bash` tool output as a one-line preview; toggle temporarily with `/compact-bash`.
|
|
7
9
|
- [`permission-gate`](./extensions/permission-gate): Prompts for confirmation before dangerous bash commands like `rm -rf`, `sudo`, and `chmod 777`.
|
|
8
10
|
- [`confirm-destructive`](./extensions/confirm-destructive): Confirms before destructive session actions like clear, switch, and fork.
|
|
9
11
|
- [`notify`](./extensions/notify): Sends configurable terminal, desktop, bell, and sound notifications when pi finishes and is ready for input.
|
|
@@ -21,7 +23,7 @@ pi install npm:@diegopetrucci/pi-extensions
|
|
|
21
23
|
Or pin the GitHub package to this release:
|
|
22
24
|
|
|
23
25
|
```bash
|
|
24
|
-
pi install git:github.com/diegopetrucci/pi-extensions@v0.1.
|
|
26
|
+
pi install git:github.com/diegopetrucci/pi-extensions@v0.1.16
|
|
25
27
|
```
|
|
26
28
|
|
|
27
29
|
Or a specific extension:
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# compact-bash
|
|
2
|
+
|
|
3
|
+
A pi extension that makes collapsed assistant `bash` tool output much quieter.
|
|
4
|
+
|
|
5
|
+
When enabled, collapsed bash output renders as one output line plus an inline hidden-line count and `Ctrl+O` expand hint. Expanding with `Ctrl+O` still shows the full rendered output.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
/compact-bash status
|
|
11
|
+
/compact-bash off
|
|
12
|
+
/compact-bash on
|
|
13
|
+
/compact-bash toggle
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The extension starts enabled by default. Disabling is temporary for the current extension runtime/session; after `/reload`, `/new`, `/resume`, or `/fork`, it starts enabled again.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
### Standalone npm package
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pi install npm:@diegopetrucci/pi-compact-bash
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Collection package
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pi install npm:@diegopetrucci/pi-extensions
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### GitHub package
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pi install git:github.com/diegopetrucci/pi-extensions
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then reload pi:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
/reload
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Notes
|
|
45
|
+
|
|
46
|
+
- This extension overrides pi's built-in `bash` tool so it can customize only the TUI renderer.
|
|
47
|
+
- It reuses pi's built-in bash implementation and preserves `shellPath`/`shellCommandPrefix` settings when they are available from settings files.
|
|
48
|
+
- It does not truncate or rewrite the actual tool result sent to the model.
|
|
49
|
+
- It affects assistant-invoked `bash` tool rows. User `!`/`!!` bash commands are rendered by a separate pi component and keep pi's default preview behavior.
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBashToolDefinition,
|
|
3
|
+
DEFAULT_MAX_BYTES,
|
|
4
|
+
formatSize,
|
|
5
|
+
keyHint,
|
|
6
|
+
SettingsManager,
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Container, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
10
|
+
|
|
11
|
+
const COLLAPSED_PREVIEW_LINES = 1;
|
|
12
|
+
|
|
13
|
+
type BashToolDefinition = ReturnType<typeof createBashToolDefinition>;
|
|
14
|
+
type BashRenderResult = NonNullable<BashToolDefinition["renderResult"]>;
|
|
15
|
+
type BashRenderResultParams = Parameters<BashRenderResult>;
|
|
16
|
+
type BashRenderState = {
|
|
17
|
+
endedAt?: number;
|
|
18
|
+
interval?: ReturnType<typeof setInterval>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getTextOutput(result: BashRenderResultParams[0]): string {
|
|
22
|
+
return result.content
|
|
23
|
+
.filter((content) => content.type === "text")
|
|
24
|
+
.map((content) => content.text ?? "")
|
|
25
|
+
.join("\n")
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatDuration(ms: number): string {
|
|
30
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function visibleLength(text: string): number {
|
|
34
|
+
return text.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").length;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function renderCollapsedLine(
|
|
38
|
+
line: string,
|
|
39
|
+
hiddenLines: number,
|
|
40
|
+
theme: BashRenderResultParams[2],
|
|
41
|
+
width: number,
|
|
42
|
+
): string {
|
|
43
|
+
if (width <= 0) return "";
|
|
44
|
+
|
|
45
|
+
const styledLine = theme.fg("toolOutput", line);
|
|
46
|
+
if (hiddenLines <= 0) {
|
|
47
|
+
return truncateToWidth(styledLine, width, "...");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const hint = theme.fg(
|
|
51
|
+
"muted",
|
|
52
|
+
` ... ${hiddenLines} more line${hiddenLines === 1 ? "" : "s"} (${keyHint("app.tools.expand", "to expand")})`,
|
|
53
|
+
);
|
|
54
|
+
const hintWidth = visibleLength(hint);
|
|
55
|
+
if (hintWidth + 8 > width) {
|
|
56
|
+
return truncateToWidth(`${styledLine}${hint}`, width, "...");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return `${truncateToWidth(styledLine, width - hintWidth, "...")}${hint}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getTruncationWarnings(result: BashRenderResultParams[0]): string[] {
|
|
63
|
+
const details = result.details as
|
|
64
|
+
| {
|
|
65
|
+
truncation?: {
|
|
66
|
+
truncated?: boolean;
|
|
67
|
+
truncatedBy?: "lines" | "bytes";
|
|
68
|
+
outputLines?: number;
|
|
69
|
+
totalLines?: number;
|
|
70
|
+
maxBytes?: number;
|
|
71
|
+
};
|
|
72
|
+
fullOutputPath?: string;
|
|
73
|
+
}
|
|
74
|
+
| undefined;
|
|
75
|
+
const truncation = details?.truncation;
|
|
76
|
+
const warnings: string[] = [];
|
|
77
|
+
|
|
78
|
+
if (details?.fullOutputPath) {
|
|
79
|
+
warnings.push(`Full output: ${details.fullOutputPath}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (truncation?.truncated) {
|
|
83
|
+
if (truncation.truncatedBy === "lines") {
|
|
84
|
+
warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
|
|
85
|
+
} else {
|
|
86
|
+
warnings.push(
|
|
87
|
+
`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return warnings;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function syncElapsedTimer(options: BashRenderResultParams[1], context: BashRenderResultParams[3]): BashRenderState {
|
|
96
|
+
const state = context.state as BashRenderState & { startedAt?: number };
|
|
97
|
+
|
|
98
|
+
if (state.startedAt !== undefined && options.isPartial && !state.interval) {
|
|
99
|
+
state.interval = setInterval(() => context.invalidate(), 1000);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!options.isPartial || context.isError) {
|
|
103
|
+
state.endedAt ??= Date.now();
|
|
104
|
+
if (state.interval) {
|
|
105
|
+
clearInterval(state.interval);
|
|
106
|
+
state.interval = undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return state;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const renderCompactBashResult: BashRenderResult = (result, options, theme, context) => {
|
|
114
|
+
const state = syncElapsedTimer(options, context);
|
|
115
|
+
const component = (context.lastComponent as Container | undefined) ?? new Container();
|
|
116
|
+
component.clear();
|
|
117
|
+
|
|
118
|
+
const output = getTextOutput(result);
|
|
119
|
+
const outputLines = output ? output.split("\n") : [];
|
|
120
|
+
|
|
121
|
+
if (outputLines.length > 0) {
|
|
122
|
+
if (options.expanded) {
|
|
123
|
+
const displayText = outputLines.map((line) => theme.fg("toolOutput", line)).join("\n");
|
|
124
|
+
component.addChild(new Text(displayText, 0, 0));
|
|
125
|
+
} else {
|
|
126
|
+
const firstLine = outputLines[0] ?? "";
|
|
127
|
+
const hiddenLines = Math.max(0, outputLines.length - COLLAPSED_PREVIEW_LINES);
|
|
128
|
+
component.addChild({
|
|
129
|
+
render: (width) => [renderCollapsedLine(firstLine, hiddenLines, theme, width)],
|
|
130
|
+
invalidate: () => undefined,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
} else if (options.isPartial) {
|
|
134
|
+
component.addChild(new Text(theme.fg("muted", "Running..."), 0, 0));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const warnings = getTruncationWarnings(result);
|
|
138
|
+
if (warnings.length > 0) {
|
|
139
|
+
component.addChild(new Text(theme.fg("warning", `[${warnings.join(". ")}]`), 0, 0));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (options.expanded) {
|
|
143
|
+
const statusParts: string[] = [];
|
|
144
|
+
if (outputLines.length > COLLAPSED_PREVIEW_LINES) {
|
|
145
|
+
statusParts.push(`(${keyHint("app.tools.expand", "to collapse")})`);
|
|
146
|
+
}
|
|
147
|
+
if (state.startedAt !== undefined) {
|
|
148
|
+
const endTime = state.endedAt ?? Date.now();
|
|
149
|
+
statusParts.push(theme.fg("muted", `${options.isPartial ? "Elapsed" : "Took"} ${formatDuration(endTime - state.startedAt)}`));
|
|
150
|
+
}
|
|
151
|
+
if (statusParts.length > 0) {
|
|
152
|
+
component.addChild(new Text(statusParts.join("\n"), 0, 0));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
component.invalidate();
|
|
157
|
+
return component;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
function createBaseBashToolDefinition(cwd: string): BashToolDefinition {
|
|
161
|
+
try {
|
|
162
|
+
const settings = SettingsManager.create(cwd);
|
|
163
|
+
return createBashToolDefinition(cwd, {
|
|
164
|
+
commandPrefix: settings.getShellCommandPrefix(),
|
|
165
|
+
shellPath: settings.getShellPath(),
|
|
166
|
+
});
|
|
167
|
+
} catch {
|
|
168
|
+
return createBashToolDefinition(cwd);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function createCompactBashToolDefinition(cwd: string, enabled: boolean): BashToolDefinition {
|
|
173
|
+
const base = createBaseBashToolDefinition(cwd);
|
|
174
|
+
if (!enabled) return base;
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
...base,
|
|
178
|
+
renderResult: renderCompactBashResult,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export default function compactBashExtension(pi: ExtensionAPI) {
|
|
183
|
+
let enabled = true;
|
|
184
|
+
|
|
185
|
+
function registerBashTool(cwd: string): void {
|
|
186
|
+
pi.registerTool(createCompactBashToolDefinition(cwd, enabled));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
190
|
+
registerBashTool(ctx.cwd);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
pi.registerCommand("compact-bash", {
|
|
194
|
+
description: "Toggle one-line collapsed previews for bash tool output",
|
|
195
|
+
getArgumentCompletions: (prefix) => {
|
|
196
|
+
const commands = ["on", "off", "toggle", "status"];
|
|
197
|
+
const query = prefix.trim().toLowerCase();
|
|
198
|
+
const matches = commands.filter((command) => command.startsWith(query));
|
|
199
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
200
|
+
},
|
|
201
|
+
handler: async (args, ctx) => {
|
|
202
|
+
const action = args.trim().toLowerCase() || "toggle";
|
|
203
|
+
|
|
204
|
+
if (action === "on" || action === "enable") {
|
|
205
|
+
enabled = true;
|
|
206
|
+
registerBashTool(ctx.cwd);
|
|
207
|
+
ctx.ui.notify("Compact bash previews enabled: collapsed bash output shows one line.", "info");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (action === "off" || action === "disable") {
|
|
212
|
+
enabled = false;
|
|
213
|
+
registerBashTool(ctx.cwd);
|
|
214
|
+
ctx.ui.notify("Compact bash previews disabled: restored pi's standard bash renderer.", "info");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (action === "toggle") {
|
|
219
|
+
enabled = !enabled;
|
|
220
|
+
registerBashTool(ctx.cwd);
|
|
221
|
+
ctx.ui.notify(
|
|
222
|
+
enabled
|
|
223
|
+
? "Compact bash previews enabled: collapsed bash output shows one line."
|
|
224
|
+
: "Compact bash previews disabled: restored pi's standard bash renderer.",
|
|
225
|
+
"info",
|
|
226
|
+
);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (action === "status") {
|
|
231
|
+
ctx.ui.notify(
|
|
232
|
+
`Compact bash previews are ${enabled ? "enabled" : "disabled"}. Collapsed preview lines: ${enabled ? COLLAPSED_PREVIEW_LINES : "pi default"}.`,
|
|
233
|
+
"info",
|
|
234
|
+
);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
ctx.ui.notify("Usage: /compact-bash on | off | toggle | status", "warning");
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@diegopetrucci/pi-compact-bash",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A pi extension that renders collapsed bash tool output as a one-line preview.",
|
|
5
|
+
"keywords": ["pi-package", "pi", "bash", "terminal"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/diegopetrucci/pi-extensions.git",
|
|
10
|
+
"directory": "extensions/compact-bash"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.ts",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"pi": {
|
|
20
|
+
"extensions": [
|
|
21
|
+
"index.ts"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
26
|
+
"@earendil-works/pi-tui": "*"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# context-cap
|
|
2
|
+
|
|
3
|
+
A pi extension that treats large-context models as having an effective 200k-token context window, so pi's built-in auto-compaction starts earlier, and avoids the dumb zone.
|
|
4
|
+
|
|
5
|
+
By default, pi auto-compacts when:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
contextTokens > model.contextWindow - reserveTokens
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This extension changes the active model's in-memory `contextWindow` to:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
min(originalContextWindow, 200000)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
With pi's default `reserveTokens` of 16,384, models larger than 200k will proactively compact around 183,616 tokens.
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
/context-cap status
|
|
23
|
+
/context-cap off
|
|
24
|
+
/context-cap on
|
|
25
|
+
/context-cap toggle
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The extension starts enabled by default. Disabling is temporary for the current extension runtime/session; after `/reload`, `/new`, `/resume`, or `/fork`, the extension starts enabled again.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
### Standalone npm package
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pi install npm:@diegopetrucci/pi-context-cap
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Collection package
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pi install npm:@diegopetrucci/pi-extensions
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### GitHub package
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pi install git:github.com/diegopetrucci/pi-extensions
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then reload pi:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
/reload
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Notes
|
|
57
|
+
|
|
58
|
+
- This extension mutates pi's in-memory model metadata only. It does not edit `models.json`.
|
|
59
|
+
- The cap affects pi logic that reads `model.contextWindow`, including auto-compaction thresholding and UI context-window display.
|
|
60
|
+
- Because pi also uses `model.contextWindow` for some overflow detection, a request that succeeds above 200k tokens on a larger model may be treated as overflow and retried after compaction. Use `/context-cap off` if you need the full model window temporarily.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Model, Api } from "@earendil-works/pi-ai";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MAX_CONTEXT_WINDOW = 200_000;
|
|
5
|
+
type AnyModel = Model<Api> | Model<any>;
|
|
6
|
+
|
|
7
|
+
const originalContextWindows = new WeakMap<AnyModel, number>();
|
|
8
|
+
const touchedModels = new Set<AnyModel>();
|
|
9
|
+
|
|
10
|
+
type ApplyResult = {
|
|
11
|
+
changed: boolean;
|
|
12
|
+
key: string;
|
|
13
|
+
original: number;
|
|
14
|
+
effective: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function modelKey(model: AnyModel): string {
|
|
18
|
+
return `${model.provider}/${model.id}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getOriginalContextWindow(model: AnyModel): number {
|
|
22
|
+
const existing = originalContextWindows.get(model);
|
|
23
|
+
if (typeof existing === "number") return existing;
|
|
24
|
+
|
|
25
|
+
originalContextWindows.set(model, model.contextWindow);
|
|
26
|
+
touchedModels.add(model);
|
|
27
|
+
return model.contextWindow;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getEffectiveContextWindow(model: AnyModel): number {
|
|
31
|
+
return Math.min(getOriginalContextWindow(model), DEFAULT_MAX_CONTEXT_WINDOW);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function applyContextCap(model: AnyModel | undefined): ApplyResult | undefined {
|
|
35
|
+
if (!model) return undefined;
|
|
36
|
+
|
|
37
|
+
const key = modelKey(model);
|
|
38
|
+
const original = getOriginalContextWindow(model);
|
|
39
|
+
const effective = Math.min(original, DEFAULT_MAX_CONTEXT_WINDOW);
|
|
40
|
+
const changed = model.contextWindow !== effective;
|
|
41
|
+
model.contextWindow = effective;
|
|
42
|
+
|
|
43
|
+
return { changed, key, original, effective };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function restoreContextWindow(model: AnyModel | undefined): boolean {
|
|
47
|
+
if (!model) return false;
|
|
48
|
+
|
|
49
|
+
const original = originalContextWindows.get(model);
|
|
50
|
+
if (typeof original !== "number" || model.contextWindow === original) return false;
|
|
51
|
+
|
|
52
|
+
model.contextWindow = original;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function forEachRegistryModel(ctx: ExtensionContext, callback: (model: AnyModel) => void): void {
|
|
57
|
+
try {
|
|
58
|
+
for (const model of ctx.modelRegistry.getAll()) {
|
|
59
|
+
callback(model);
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// Best effort only. The active ctx.model is handled separately by callers.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function applyContextCapToSession(ctx: ExtensionContext): number {
|
|
67
|
+
let changed = 0;
|
|
68
|
+
|
|
69
|
+
forEachRegistryModel(ctx, (model) => {
|
|
70
|
+
if (applyContextCap(model)?.changed) changed++;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (applyContextCap(ctx.model)?.changed) changed++;
|
|
74
|
+
return changed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function restoreContextCapForSession(ctx: ExtensionContext): number {
|
|
78
|
+
let changed = 0;
|
|
79
|
+
|
|
80
|
+
forEachRegistryModel(ctx, (model) => {
|
|
81
|
+
if (restoreContextWindow(model)) changed++;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (restoreContextWindow(ctx.model)) changed++;
|
|
85
|
+
return changed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatTokens(tokens: number): string {
|
|
89
|
+
return tokens >= 1000 ? `${Math.round(tokens / 1000)}k` : String(tokens);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default function contextCapExtension(pi: ExtensionAPI) {
|
|
93
|
+
let enabled = true;
|
|
94
|
+
|
|
95
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
96
|
+
if (enabled) applyContextCapToSession(ctx);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
pi.on("model_select", async (event, ctx) => {
|
|
100
|
+
if (!enabled) return;
|
|
101
|
+
|
|
102
|
+
const result = applyContextCap(event.model);
|
|
103
|
+
if (!result || !ctx.hasUI) return;
|
|
104
|
+
|
|
105
|
+
ctx.ui.setStatus(
|
|
106
|
+
"context-cap",
|
|
107
|
+
result.original > DEFAULT_MAX_CONTEXT_WINDOW
|
|
108
|
+
? `ctx cap ${formatTokens(result.effective)}/${formatTokens(result.original)}`
|
|
109
|
+
: undefined,
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
114
|
+
restoreContextCapForSession(ctx);
|
|
115
|
+
for (const model of touchedModels) restoreContextWindow(model);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
pi.registerCommand("context-cap", {
|
|
119
|
+
description: "Toggle the 200k effective context-window cap for auto-compaction",
|
|
120
|
+
getArgumentCompletions: (prefix) => {
|
|
121
|
+
const commands = ["on", "off", "toggle", "status"];
|
|
122
|
+
const matches = commands.filter((command) => command.startsWith(prefix.trim()));
|
|
123
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
124
|
+
},
|
|
125
|
+
handler: async (args, ctx) => {
|
|
126
|
+
const action = args.trim().toLowerCase() || "toggle";
|
|
127
|
+
|
|
128
|
+
if (action === "on" || action === "enable") {
|
|
129
|
+
enabled = true;
|
|
130
|
+
const changed = applyContextCapToSession(ctx);
|
|
131
|
+
ctx.ui.setStatus("context-cap", `ctx cap ${formatTokens(DEFAULT_MAX_CONTEXT_WINDOW)}`);
|
|
132
|
+
ctx.ui.notify(`Context cap enabled (${changed} model window(s) capped/restored).`, "info");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (action === "off" || action === "disable") {
|
|
137
|
+
enabled = false;
|
|
138
|
+
const changed = restoreContextCapForSession(ctx);
|
|
139
|
+
ctx.ui.setStatus("context-cap", undefined);
|
|
140
|
+
ctx.ui.notify(`Context cap disabled for this extension session (${changed} model window(s) restored).`, "info");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (action === "toggle") {
|
|
145
|
+
if (enabled) {
|
|
146
|
+
enabled = false;
|
|
147
|
+
const changed = restoreContextCapForSession(ctx);
|
|
148
|
+
ctx.ui.setStatus("context-cap", undefined);
|
|
149
|
+
ctx.ui.notify(`Context cap disabled for this extension session (${changed} model window(s) restored).`, "info");
|
|
150
|
+
} else {
|
|
151
|
+
enabled = true;
|
|
152
|
+
const changed = applyContextCapToSession(ctx);
|
|
153
|
+
ctx.ui.setStatus("context-cap", `ctx cap ${formatTokens(DEFAULT_MAX_CONTEXT_WINDOW)}`);
|
|
154
|
+
ctx.ui.notify(`Context cap enabled (${changed} model window(s) capped/restored).`, "info");
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (action === "status") {
|
|
160
|
+
const model = ctx.model;
|
|
161
|
+
const status = enabled ? "enabled" : "disabled";
|
|
162
|
+
if (!model) {
|
|
163
|
+
ctx.ui.notify(`Context cap is ${status}. No model selected.`, "info");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const original = getOriginalContextWindow(model);
|
|
168
|
+
const effective = enabled ? getEffectiveContextWindow(model) : model.contextWindow;
|
|
169
|
+
ctx.ui.notify(
|
|
170
|
+
`Context cap is ${status}. Current model: ${modelKey(model)} (${formatTokens(effective)}/${formatTokens(original)} effective/original).`,
|
|
171
|
+
"info",
|
|
172
|
+
);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
ctx.ui.notify("Usage: /context-cap on | off | toggle | status", "warning");
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@diegopetrucci/pi-context-cap",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A pi extension that caps effective model context windows at 200k tokens for earlier auto-compaction.",
|
|
5
|
+
"keywords": ["pi-package", "pi", "context", "compaction"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/diegopetrucci/pi-extensions.git",
|
|
10
|
+
"directory": "extensions/context-cap"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.ts",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"pi": {
|
|
20
|
+
"extensions": [
|
|
21
|
+
"index.ts"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@earendil-works/pi-ai": "*",
|
|
26
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
27
|
+
}
|
|
28
|
+
}
|
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 minimal custom footer, an Amp-style oracle, a permission gate for dangerous bash commands, confirm-before-destructive session actions, and terminal notifications when pi is ready for input.",
|
|
3
|
+
"version": "0.1.16",
|
|
4
|
+
"description": "A collection of pi extensions, including a minimal custom footer, an Amp-style oracle, a 200k context cap for auto-compaction, one-line collapsed bash 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": {
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
"extensions": [
|
|
31
31
|
"./extensions/minimal-footer/index.ts",
|
|
32
32
|
"./extensions/oracle/index.ts",
|
|
33
|
+
"./extensions/context-cap/index.ts",
|
|
34
|
+
"./extensions/compact-bash/index.ts",
|
|
33
35
|
"./extensions/permission-gate/index.ts",
|
|
34
36
|
"./extensions/confirm-destructive/index.ts",
|
|
35
37
|
"./extensions/notify/index.ts"
|