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