@diegopetrucci/pi-extensions 0.1.73 → 0.1.76
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/LICENSE +2 -0
- package/README.md +9 -4
- package/extensions/agent-workflow-audit/index.ts +1398 -1009
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/README.md +3 -1
- package/extensions/annotate-git-diff/clipboard.ts +109 -100
- package/extensions/annotate-git-diff/git.ts +853 -767
- package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
- package/extensions/annotate-git-diff/index.ts +712 -671
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-git-diff/prompt.ts +45 -45
- package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
- package/extensions/annotate-git-diff/review-server.ts +337 -311
- package/extensions/annotate-git-diff/types.ts +132 -132
- package/extensions/annotate-git-diff/ui.ts +27 -27
- package/extensions/annotate-git-diff/watch.ts +74 -74
- package/extensions/annotate-git-diff/web/app.js +1947 -1884
- package/extensions/annotate-git-diff/web/index.html +5 -6
- package/extensions/annotate-last-message/README.md +1 -1
- package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
- package/extensions/annotate-last-message/index.ts +210 -206
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/prompt.ts +87 -66
- package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
- package/extensions/annotate-last-message/session.ts +94 -89
- package/extensions/annotate-last-message/theme.ts +199 -193
- package/extensions/annotate-last-message/types.ts +23 -23
- package/extensions/annotate-last-message/ui.ts +27 -25
- package/extensions/annotate-last-message/web/app.js +4 -7
- package/extensions/annotate-last-message/web/md-renderer.js +1 -1
- package/extensions/brrr/README.md +1 -1
- package/extensions/brrr/index.ts +213 -191
- package/extensions/brrr/package.json +1 -1
- package/extensions/claude-fast/README.md +5 -79
- package/extensions/claude-fast/index.ts +31 -289
- package/extensions/claude-fast/package.json +2 -3
- package/extensions/code-reviewer/README.md +1 -1
- package/extensions/code-reviewer/index.ts +1598 -1350
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/README.md +1 -1
- package/extensions/confirm-destructive/index.ts +55 -51
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/index.ts +312 -304
- package/extensions/context-cap/package.json +1 -1
- package/extensions/context-inspector/index.ts +1211 -1064
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/index.ts +1854 -1735
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/index.ts +41 -41
- package/extensions/dynamic-context-pruning/README.md +6 -38
- package/extensions/dynamic-context-pruning/index.ts +2124 -1980
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +71 -21
- package/extensions/fast/index.ts +513 -313
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/README.md +1 -1
- package/extensions/git-footer/index.ts +458 -411
- package/extensions/git-footer/package.json +2 -3
- package/extensions/gnosis/index.ts +180 -130
- package/extensions/gnosis/package.json +1 -1
- package/extensions/inline-bash/index.ts +88 -88
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/index.ts +1669 -1419
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/index.ts +994 -1012
- package/extensions/minimal-footer/openai-usage.ts +195 -190
- package/extensions/notify/index.ts +222 -230
- package/extensions/notify/package.json +1 -1
- package/extensions/openai-fast/README.md +5 -73
- package/extensions/openai-fast/index.ts +32 -272
- package/extensions/openai-fast/package.json +2 -3
- package/extensions/oracle/index.ts +1731 -1625
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/index.ts +658 -608
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/permission-gate/powershell-safety.ts +840 -793
- package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
- package/extensions/project-mcp-json/README.md +199 -0
- package/extensions/project-mcp-json/config.ts +564 -0
- package/extensions/project-mcp-json/index.ts +119 -0
- package/extensions/project-mcp-json/package.json +38 -0
- package/extensions/quiet-tools/index.ts +282 -264
- package/extensions/review/README.md +2 -2
- package/extensions/review/index.ts +1745 -1872
- package/extensions/review/package.json +1 -1
- package/extensions/todo/index.ts +284 -265
- package/extensions/triage-comments/README.md +2 -2
- package/extensions/triage-comments/index.ts +2294 -1882
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +6 -3
- package/assets/social-preview.png +0 -0
- package/extensions/claude-fast/claude-fast.example.json +0 -4
- package/extensions/openai-fast/openai-fast.example.json +0 -4
|
@@ -9,9 +9,9 @@ type AnyModel = Model<Api> | Model<any>;
|
|
|
9
9
|
type ModelWindow = number | undefined;
|
|
10
10
|
type HolderToken = object;
|
|
11
11
|
type ModelHold = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
original: number;
|
|
13
|
+
cap: number;
|
|
14
|
+
holders: Set<HolderToken>;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
type GlobalSymbolTable = typeof globalThis & { [key: symbol]: unknown };
|
|
@@ -20,363 +20,371 @@ type GlobalSymbolTable = typeof globalThis & { [key: symbol]: unknown };
|
|
|
20
20
|
// a Symbol.for slot so separately loaded module copies share one WeakMap, while
|
|
21
21
|
// the WeakMap itself does not retain model objects after every holder releases.
|
|
22
22
|
function getProcessModelHolds(): WeakMap<AnyModel, ModelHold> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const globalObject = globalThis as GlobalSymbolTable;
|
|
24
|
+
const existing = globalObject[PROCESS_STATE_KEY];
|
|
25
|
+
if (existing instanceof WeakMap) return existing as WeakMap<AnyModel, ModelHold>;
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const created = new WeakMap<AnyModel, ModelHold>();
|
|
28
|
+
globalObject[PROCESS_STATE_KEY] = created;
|
|
29
|
+
return created;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const processModelHolds = getProcessModelHolds();
|
|
33
33
|
|
|
34
34
|
type CapReason = "virtual" | "unknown" | "reserve" | "modelOverride" | "settingsUnavailable";
|
|
35
35
|
type ApplyResult = {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
changed: boolean;
|
|
37
|
+
key: string;
|
|
38
|
+
original?: number;
|
|
39
|
+
effective?: number;
|
|
40
|
+
reserveTokens?: number;
|
|
41
|
+
holders?: number;
|
|
42
|
+
reason?: CapReason;
|
|
43
43
|
};
|
|
44
44
|
type CapGuard =
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
| { allowed: true }
|
|
46
|
+
| { allowed: false; reason: Exclude<CapReason, "virtual" | "unknown">; reserveTokens?: number };
|
|
47
47
|
|
|
48
48
|
function modelKey(model: AnyModel): string {
|
|
49
|
-
|
|
49
|
+
return `${model.provider}/${model.id}`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
53
|
-
|
|
53
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function isKnownContextWindow(value: unknown): value is number {
|
|
57
|
-
|
|
57
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function isNonNegativeSafeInteger(value: unknown): value is number {
|
|
61
|
-
|
|
61
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function isVirtualModel(model: AnyModel): boolean {
|
|
65
|
-
|
|
65
|
+
return (model as AnyModel & { api?: string }).api === "pi-virtual";
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function readCapGuard(pi: ExtensionAPI, model: AnyModel): CapGuard {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
69
|
+
const getSettings = (pi as ExtensionAPI & { getSettings?: () => unknown }).getSettings;
|
|
70
|
+
// Pi before getSettings existed has no way to expose the effective
|
|
71
|
+
// compaction settings. Preserve the older-host cap behavior there.
|
|
72
|
+
if (typeof getSettings !== "function") return { allowed: true };
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const settings = getSettings.call(pi);
|
|
76
|
+
if (!isRecord(settings)) return { allowed: false, reason: "settingsUnavailable" };
|
|
77
|
+
const compactionValue = settings.compaction;
|
|
78
|
+
if (compactionValue === undefined) return { allowed: true };
|
|
79
|
+
if (!isRecord(compactionValue)) return { allowed: false, reason: "settingsUnavailable" };
|
|
80
|
+
|
|
81
|
+
const overridesValue = compactionValue.modelOverrides;
|
|
82
|
+
if (overridesValue !== undefined && !isRecord(overridesValue)) {
|
|
83
|
+
return { allowed: false, reason: "settingsUnavailable" };
|
|
84
|
+
}
|
|
85
|
+
if (overridesValue && Object.prototype.hasOwnProperty.call(overridesValue, modelKey(model))) {
|
|
86
|
+
return { allowed: false, reason: "modelOverride" };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ordinaryReserve = compactionValue.reserveTokens;
|
|
90
|
+
if (ordinaryReserve !== undefined && !isNonNegativeSafeInteger(ordinaryReserve)) {
|
|
91
|
+
return { allowed: false, reason: "settingsUnavailable" };
|
|
92
|
+
}
|
|
93
|
+
const reserveTokens = ordinaryReserve ?? DEFAULT_RESERVE_TOKENS;
|
|
94
|
+
return reserveTokens >= DEFAULT_MAX_CONTEXT_WINDOW
|
|
95
|
+
? { allowed: false, reason: "reserve", reserveTokens }
|
|
96
|
+
: { allowed: true };
|
|
97
|
+
} catch {
|
|
98
|
+
// A host settings implementation that cannot return a coherent snapshot
|
|
99
|
+
// must not be guessed at: leave the model unmodified and report why.
|
|
100
|
+
return { allowed: false, reason: "settingsUnavailable" };
|
|
101
|
+
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
function originalContextWindow(model: AnyModel): ModelWindow {
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
const hold = processModelHolds.get(model);
|
|
106
|
+
return (
|
|
107
|
+
hold?.original ?? (isKnownContextWindow(model.contextWindow) ? model.contextWindow : undefined)
|
|
108
|
+
);
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
function releaseContextCap(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
111
|
+
function releaseContextCap(
|
|
112
|
+
model: AnyModel | undefined,
|
|
113
|
+
holderToken: HolderToken,
|
|
114
|
+
heldModels: Set<AnyModel>,
|
|
115
|
+
): boolean {
|
|
116
|
+
if (!model || !heldModels.delete(model)) return false;
|
|
117
|
+
|
|
118
|
+
const hold = processModelHolds.get(model);
|
|
119
|
+
if (!hold) return false;
|
|
120
|
+
hold.holders.delete(holderToken);
|
|
121
|
+
if (hold.holders.size > 0) return false;
|
|
122
|
+
|
|
123
|
+
processModelHolds.delete(model);
|
|
124
|
+
// Do not overwrite a value changed by the host or another extension while
|
|
125
|
+
// this holder was active. Equality is the only safe signal available here.
|
|
126
|
+
if (model.contextWindow !== hold.cap) return false;
|
|
127
|
+
model.contextWindow = hold.original;
|
|
128
|
+
return true;
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
function applyContextCap(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
pi: ExtensionAPI,
|
|
133
|
+
model: AnyModel | undefined,
|
|
134
|
+
holderToken: HolderToken,
|
|
135
|
+
heldModels: Set<AnyModel>,
|
|
130
136
|
): ApplyResult | undefined {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
137
|
+
if (!model) return undefined;
|
|
138
|
+
|
|
139
|
+
const key = modelKey(model);
|
|
140
|
+
if (isVirtualModel(model)) {
|
|
141
|
+
return { changed: false, key, reason: "virtual" };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const hold = processModelHolds.get(model);
|
|
145
|
+
if (!isKnownContextWindow(model.contextWindow) && !hold) {
|
|
146
|
+
return { changed: false, key, reason: "unknown" };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const original = hold?.original ?? model.contextWindow;
|
|
150
|
+
if (!isKnownContextWindow(original)) {
|
|
151
|
+
return { changed: false, key, reason: "unknown" };
|
|
152
|
+
}
|
|
153
|
+
const effective = hold?.cap ?? Math.min(original, DEFAULT_MAX_CONTEXT_WINDOW);
|
|
154
|
+
if (!hold && original <= DEFAULT_MAX_CONTEXT_WINDOW) {
|
|
155
|
+
return { changed: false, key, original, effective };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const guard = readCapGuard(pi, model);
|
|
159
|
+
if (!guard.allowed) {
|
|
160
|
+
if (heldModels.has(model)) releaseContextCap(model, holderToken, heldModels);
|
|
161
|
+
return {
|
|
162
|
+
changed: false,
|
|
163
|
+
key,
|
|
164
|
+
original,
|
|
165
|
+
effective,
|
|
166
|
+
reserveTokens: guard.reserveTokens,
|
|
167
|
+
reason: guard.reason,
|
|
168
|
+
holders: hold?.holders.size,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const activeHold = hold ?? { original, cap: effective, holders: new Set<HolderToken>() };
|
|
173
|
+
if (!hold) processModelHolds.set(model, activeHold);
|
|
174
|
+
const alreadyHeld = heldModels.has(model);
|
|
175
|
+
if (!alreadyHeld) {
|
|
176
|
+
activeHold.holders.add(holderToken);
|
|
177
|
+
heldModels.add(model);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Only the first holder writes the cap. If the host changed a shared model
|
|
181
|
+
// after that, later holders join the reference count without clobbering it.
|
|
182
|
+
const changed = !hold && model.contextWindow !== activeHold.cap;
|
|
183
|
+
if (changed) model.contextWindow = activeHold.cap;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
changed,
|
|
187
|
+
key,
|
|
188
|
+
original: activeHold.original,
|
|
189
|
+
effective: activeHold.cap,
|
|
190
|
+
holders: activeHold.holders.size,
|
|
191
|
+
};
|
|
186
192
|
}
|
|
187
193
|
|
|
188
194
|
function restoreTouchedModels(holderToken: HolderToken, heldModels: Set<AnyModel>): number {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
let changed = 0;
|
|
196
|
+
for (const model of [...heldModels]) {
|
|
197
|
+
if (releaseContextCap(model, holderToken, heldModels)) changed++;
|
|
198
|
+
}
|
|
199
|
+
return changed;
|
|
194
200
|
}
|
|
195
201
|
|
|
196
202
|
function applyContextCapToSession(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
pi: ExtensionAPI,
|
|
204
|
+
ctx: ExtensionContext,
|
|
205
|
+
holderToken: HolderToken,
|
|
206
|
+
heldModels: Set<AnyModel>,
|
|
201
207
|
): number {
|
|
202
|
-
|
|
208
|
+
return applyContextCap(pi, ctx.model, holderToken, heldModels)?.changed ? 1 : 0;
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
function formatTokens(tokens: number): string {
|
|
206
|
-
|
|
212
|
+
return tokens >= 1000 ? `${Math.round(tokens / 1000)}k` : String(tokens);
|
|
207
213
|
}
|
|
208
214
|
|
|
209
215
|
function formatWindow(window: ModelWindow): string {
|
|
210
|
-
|
|
216
|
+
return isKnownContextWindow(window) ? formatTokens(window) : "unknown";
|
|
211
217
|
}
|
|
212
218
|
|
|
213
219
|
function unavailableReason(result: ApplyResult | undefined): string | undefined {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
220
|
+
if (result?.reason === "virtual") {
|
|
221
|
+
return "virtual selection; routed physical limit is unavailable to this extension";
|
|
222
|
+
}
|
|
223
|
+
if (result?.reason === "unknown") {
|
|
224
|
+
return "physical model has no compatible context-window limit";
|
|
225
|
+
}
|
|
226
|
+
if (result?.reason === "reserve") {
|
|
227
|
+
return `compaction reserveTokens (${formatTokens(result.reserveTokens ?? DEFAULT_MAX_CONTEXT_WINDOW)}) is at least 200k; the auto-compaction threshold would be zero or negative`;
|
|
228
|
+
}
|
|
229
|
+
if (result?.reason === "modelOverride") {
|
|
230
|
+
return `compaction.modelOverrides[\"${result.key}\"] is configured; leaving the model window unchanged`;
|
|
231
|
+
}
|
|
232
|
+
if (result?.reason === "settingsUnavailable") {
|
|
233
|
+
return "compaction settings could not be resolved safely";
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
230
236
|
}
|
|
231
237
|
|
|
232
238
|
function holdStatus(model: AnyModel, heldModels: Set<AnyModel>): string {
|
|
233
|
-
|
|
234
|
-
|
|
239
|
+
const hold = processModelHolds.get(model);
|
|
240
|
+
if (!hold) return " No process-wide context-cap hold is active.";
|
|
235
241
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
242
|
+
const otherHolders = Math.max(hold.holders.size - (heldModels.has(model) ? 1 : 0), 0);
|
|
243
|
+
const holderWord = otherHolders === 1 ? "holder" : "holders";
|
|
244
|
+
const holderVerb = otherHolders === 1 ? "holds" : "hold";
|
|
245
|
+
return ` Process-wide cap holders: ${hold.holders.size}; ${otherHolders} other ${holderWord} still ${holderVerb} the cap.`;
|
|
240
246
|
}
|
|
241
247
|
|
|
242
248
|
export default function contextCapExtension(pi: ExtensionAPI) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
249
|
+
let enabled = true;
|
|
250
|
+
const holderToken: HolderToken = {};
|
|
251
|
+
const heldModels = new Set<AnyModel>();
|
|
252
|
+
|
|
253
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
254
|
+
// A reload/session replacement can reuse model objects. Release only this
|
|
255
|
+
// extension instance's holds before capturing the new active model.
|
|
256
|
+
restoreTouchedModels(holderToken, heldModels);
|
|
257
|
+
if (enabled) applyContextCapToSession(pi, ctx, holderToken, heldModels);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
pi.on("model_select", async (event, ctx) => {
|
|
261
|
+
// Selection changes are the narrowest reliable boundary available to an
|
|
262
|
+
// extension. Release this instance's previous handle before applying the
|
|
263
|
+
// new physical model, and never infer a virtual route from its metadata.
|
|
264
|
+
releaseContextCap(event.previousModel, holderToken, heldModels);
|
|
265
|
+
if (!enabled) return;
|
|
266
|
+
|
|
267
|
+
const result = applyContextCap(pi, event.model, holderToken, heldModels);
|
|
268
|
+
if (!result || !ctx.hasUI) return;
|
|
269
|
+
|
|
270
|
+
const unavailable = unavailableReason(result);
|
|
271
|
+
ctx.ui.setStatus(
|
|
272
|
+
"context-cap",
|
|
273
|
+
unavailable
|
|
274
|
+
? "ctx cap unavailable"
|
|
275
|
+
: result.original !== undefined && result.original > DEFAULT_MAX_CONTEXT_WINDOW
|
|
276
|
+
? `ctx cap ${formatTokens(result.effective ?? result.original)}/${formatTokens(result.original)}`
|
|
277
|
+
: undefined,
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
282
|
+
// Branch changes do not expose a different routed model. Reapply only to
|
|
283
|
+
// the active physical handle after releasing this instance's old hold.
|
|
284
|
+
restoreTouchedModels(holderToken, heldModels);
|
|
285
|
+
if (enabled) applyContextCapToSession(pi, ctx, holderToken, heldModels);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
289
|
+
restoreTouchedModels(holderToken, heldModels);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
function enableContextCap(ctx: ExtensionContext): void {
|
|
293
|
+
enabled = true;
|
|
294
|
+
const result = applyContextCap(pi, ctx.model, holderToken, heldModels);
|
|
295
|
+
const changed = result?.changed ? 1 : 0;
|
|
296
|
+
const unavailable = unavailableReason(result);
|
|
297
|
+
ctx.ui.setStatus(
|
|
298
|
+
"context-cap",
|
|
299
|
+
unavailable ? "ctx cap unavailable" : `ctx cap ${formatTokens(DEFAULT_MAX_CONTEXT_WINDOW)}`,
|
|
300
|
+
);
|
|
301
|
+
ctx.ui.notify(
|
|
302
|
+
unavailable
|
|
303
|
+
? `Context cap enabled, but unavailable: ${unavailable}.`
|
|
304
|
+
: `Context cap enabled (${changed} active model window(s) capped/restored).`,
|
|
305
|
+
"info",
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function disableContextCap(ctx: ExtensionContext): void {
|
|
310
|
+
enabled = false;
|
|
311
|
+
const changed = restoreTouchedModels(holderToken, heldModels);
|
|
312
|
+
ctx.ui.setStatus("context-cap", undefined);
|
|
313
|
+
ctx.ui.notify(
|
|
314
|
+
`Context cap disabled for this extension session (${changed} active model window(s) restored).`,
|
|
315
|
+
"info",
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
pi.registerCommand("context-cap", {
|
|
320
|
+
description: "Toggle the 200k effective context-window cap for auto-compaction",
|
|
321
|
+
getArgumentCompletions: (prefix) => {
|
|
322
|
+
const commands = ["on", "off", "toggle", "status"];
|
|
323
|
+
const matches = commands.filter((command) => command.startsWith(prefix.trim()));
|
|
324
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
325
|
+
},
|
|
326
|
+
handler: async (args, ctx) => {
|
|
327
|
+
const action = args.trim().toLowerCase() || "toggle";
|
|
328
|
+
|
|
329
|
+
if (action === "on" || action === "enable") {
|
|
330
|
+
enableContextCap(ctx);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (action === "off" || action === "disable") {
|
|
335
|
+
disableContextCap(ctx);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (action === "toggle") {
|
|
340
|
+
if (enabled) disableContextCap(ctx);
|
|
341
|
+
else enableContextCap(ctx);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (action === "status") {
|
|
346
|
+
const model = ctx.model;
|
|
347
|
+
const status = enabled ? "enabled" : "disabled";
|
|
348
|
+
if (!model) {
|
|
349
|
+
ctx.ui.notify(`Context cap is ${status}. No model selected.`, "info");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (isVirtualModel(model)) {
|
|
354
|
+
ctx.ui.notify(
|
|
355
|
+
`Context cap is ${status}. Current model: ${modelKey(model)} is a virtual selection; the routed physical limit is unavailable to this extension.`,
|
|
356
|
+
"info",
|
|
357
|
+
);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const hold = processModelHolds.get(model);
|
|
362
|
+
const original = originalContextWindow(model);
|
|
363
|
+
const shouldCheckGuard =
|
|
364
|
+
enabled &&
|
|
365
|
+
(hold !== undefined ||
|
|
366
|
+
(isKnownContextWindow(model.contextWindow) &&
|
|
367
|
+
model.contextWindow > DEFAULT_MAX_CONTEXT_WINDOW));
|
|
368
|
+
const guard = shouldCheckGuard ? readCapGuard(pi, model) : { allowed: true as const };
|
|
369
|
+
const unavailable = !guard.allowed
|
|
370
|
+
? unavailableReason({
|
|
371
|
+
changed: false,
|
|
372
|
+
key: modelKey(model),
|
|
373
|
+
reason: guard.reason,
|
|
374
|
+
reserveTokens: guard.reserveTokens,
|
|
375
|
+
})
|
|
376
|
+
: undefined;
|
|
377
|
+
const prefix = unavailable
|
|
378
|
+
? `Context cap is ${status}, but unavailable: ${unavailable}.`
|
|
379
|
+
: `Context cap is ${status}.`;
|
|
380
|
+
ctx.ui.notify(
|
|
381
|
+
`${prefix} Current model: ${modelKey(model)} (${formatWindow(model.contextWindow)}/${formatWindow(original)} effective/original).${holdStatus(model, heldModels)}`,
|
|
382
|
+
"info",
|
|
383
|
+
);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
ctx.ui.notify("Usage: /context-cap on | off | toggle | status", "warning");
|
|
388
|
+
},
|
|
389
|
+
});
|
|
382
390
|
}
|