@diegopetrucci/pi-extensions 0.1.78 → 0.1.80
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 +1 -1
- package/extensions/agent-workflow-audit/index.ts +3 -20
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/glimpseui.d.ts +5 -65
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-last-message/glimpseui.d.ts +5 -65
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/prompt.ts +2 -8
- package/extensions/annotate-last-message/web/index.html +0 -27
- package/extensions/code-reviewer/index.ts +2 -46
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/context-cap/README.md +9 -9
- package/extensions/context-cap/index.ts +3 -3
- package/extensions/context-cap/package.json +2 -2
- package/extensions/context-inspector/index.ts +2 -46
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/index.ts +5 -55
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dynamic-context-pruning/README.md +24 -7
- package/extensions/dynamic-context-pruning/index.ts +21 -130
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +16 -9
- package/extensions/fast/index.ts +9 -5
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/index.ts +2 -16
- package/extensions/git-footer/package.json +1 -1
- package/extensions/librarian/index.ts +8 -46
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/README.md +5 -5
- package/extensions/minimal-footer/index.ts +3 -17
- package/extensions/minimal-footer/minimal-footer.example.json +1 -1
- package/extensions/minimal-footer/openai-usage.ts +2 -11
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/oracle/index.ts +5 -55
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/index.ts +31 -63
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/project-mcp-json/config.ts +2 -1
- package/extensions/project-mcp-json/package.json +1 -1
- package/extensions/quiet-tools/index.ts +15 -6
- package/extensions/quiet-tools/package.json +1 -1
- package/extensions/review/README.md +2 -0
- package/extensions/review/package.json +1 -1
- package/extensions/triage-comments/index.ts +1 -22
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +4 -4
|
@@ -140,12 +140,6 @@ export function normalizeSessionProjection(value: unknown): MinimalSessionProjec
|
|
|
140
140
|
return { entries, messages: entries.flatMap((entry) => entry.messages) };
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
export function sessionProjectionToMessages(
|
|
144
|
-
projection: MinimalSessionProjection,
|
|
145
|
-
): MinimalMessage[] {
|
|
146
|
-
return projection.entries.flatMap((entry) => entry.messages);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
143
|
const EXTENSION_ID = "dynamic-context-pruning";
|
|
150
144
|
const CONFIG_FILE_NAME = "dynamic-context-pruning.json";
|
|
151
145
|
const DECISION_ENTRY_TYPE = "dynamic-context-pruning:decision";
|
|
@@ -164,37 +158,10 @@ const DEFAULT_RECENT_TURNS = 4;
|
|
|
164
158
|
/** Cache cost model default: fraction of full (non-cached) price still paid for cached tokens. */
|
|
165
159
|
const DEFAULT_CACHED_PRICE_RATIO = 0.1;
|
|
166
160
|
|
|
167
|
-
/**
|
|
168
|
-
* Default net-benefit gate threshold (max amortization calls). Recalibrated
|
|
169
|
-
* from the representative-corpus benchmark (~/.the-last-harness/
|
|
170
|
-
* agent/sessions, 1,390+ session files, 556 candidates): this is the
|
|
171
|
-
* hindsight-optimal break-even T at cachedPriceRatio r=0.1 (aggressive
|
|
172
|
-
* prompt caching, the common Anthropic case). IMPORTANT caveats:
|
|
173
|
-
* - Provider/ratio-dependent: this optimum shifts higher as caching gets
|
|
174
|
-
* weaker (T=29 at r=0.25, T=54 at r=0.5, T=58 at r=0.9 on the same
|
|
175
|
-
* corpus) — see docs/v2-design.md section 4 and the extension README.
|
|
176
|
-
* - At r=0.1 the realized deterministic savings this threshold buys are
|
|
177
|
-
* economically marginal (~20.6k token-units total across the whole
|
|
178
|
-
* corpus, i.e. pennies); savings only become material at r>=0.25.
|
|
179
|
-
* - Re-derive via `scripts/benchmark.mjs <sessions-dir> --ratio 0.1` if
|
|
180
|
-
* the representative corpus changes meaningfully.
|
|
181
|
-
*/
|
|
161
|
+
/** Default idle net-benefit gate threshold; see README.md and docs/v2-design.md §4. */
|
|
182
162
|
const DEFAULT_BREAK_EVEN_THRESHOLD = 22;
|
|
183
163
|
|
|
184
|
-
/**
|
|
185
|
-
* Default mid_loop break-even threshold. The package's own
|
|
186
|
-
* representative-corpus benchmark (turn-START state definition — the same one
|
|
187
|
-
* the runtime `context` handler uses) favored rejecting mid_loop candidates
|
|
188
|
-
* at cachedPriceRatio r=0.1 (mid_loop-optimal T=1, total realized
|
|
189
|
-
* net benefit ~0.0). This supports a conservative default for the measured
|
|
190
|
-
* corpus, not a universal claim that mid_loop pruning cannot pay off.
|
|
191
|
-
* The reported 2026-09-08 byte-level capture (a mid_loop prune at T=22
|
|
192
|
-
* invalidating 81% of a warm request prefix) illustrates cache invalidation,
|
|
193
|
-
* not an inability to amortize it: enough subsequent calls can recoup a
|
|
194
|
-
* one-time bust. Idle keeps T=22; turn-start classification does not imply
|
|
195
|
-
* the provider cache is cold. Users who want the old behavior can override
|
|
196
|
-
* via gate.breakEvenThresholdByState.mid_loop.
|
|
197
|
-
*/
|
|
164
|
+
/** Default mid_loop threshold; see README.md for the runtime-aligned benchmark rationale. */
|
|
198
165
|
const DEFAULT_MID_LOOP_BREAK_EVEN_THRESHOLD = 1;
|
|
199
166
|
|
|
200
167
|
/** Tool names never pruned by default: orchestration/subagent-style tools and todo-like state. */
|
|
@@ -243,18 +210,7 @@ export interface PruneProtections {
|
|
|
243
210
|
}
|
|
244
211
|
|
|
245
212
|
export interface PruneThresholds {
|
|
246
|
-
/**
|
|
247
|
-
* Minimum raw text characters a fresh AUTOMATIC prune proposal must remove
|
|
248
|
-
* (original text chars minus placeholder chars) to be considered at all
|
|
249
|
-
* Enforced by `runDynamicContextPruningPipeline` BEFORE the
|
|
250
|
-
* net-benefit gate (see `PruneGateConfig`/`gate`, which uses break-even token
|
|
251
|
-
* math and is a separate, later check): text-only candidates below this floor
|
|
252
|
-
* are dropped up front and never reach gate/batch consideration. Image-bearing
|
|
253
|
-
* tool-result candidates bypass this raw-text floor (images receive no
|
|
254
|
-
* synthetic character count) and proceed to the token-based gate. Manual
|
|
255
|
-
* prunes and already-persisted/replayed decisions always bypass this floor,
|
|
256
|
-
* same as the gate. A value of 0 disables the floor (no filtering).
|
|
257
|
-
*/
|
|
213
|
+
/** Minimum automatic-prune text saving; see README.md Configuration for bypass rules. */
|
|
258
214
|
minCharsSaved: number;
|
|
259
215
|
}
|
|
260
216
|
|
|
@@ -287,13 +243,15 @@ export interface PruneGateConfig {
|
|
|
287
243
|
/** Fraction of full price still paid for cached ( r ). Default 0.1. */
|
|
288
244
|
cachedPriceRatio: number;
|
|
289
245
|
/**
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
246
|
+
* @deprecated Legacy global fallback retained for direct callers that pass
|
|
247
|
+
* an unnormalized gate object to resolveBreakEvenThreshold. normalizeConfig
|
|
248
|
+
* always supplies per-state values, so loaded config does not use this field
|
|
249
|
+
* to fill missing or invalid state values. Recalibrated from the
|
|
250
|
+
* representative-corpus benchmark; see DEFAULT_BREAK_EVEN_THRESHOLD for the
|
|
251
|
+
* full rationale and ratio-sensitivity caveats.
|
|
294
252
|
*/
|
|
295
253
|
breakEvenThreshold: number;
|
|
296
|
-
/** Per-state overrides.
|
|
254
|
+
/** Per-state overrides. normalizeConfig defaults to idle 22 and mid_loop 1. */
|
|
297
255
|
breakEvenThresholdByState: { idle: number; mid_loop: number };
|
|
298
256
|
}
|
|
299
257
|
|
|
@@ -493,29 +451,8 @@ export function estimateTokensForContent(
|
|
|
493
451
|
export type TokenEstimator = (text: string) => number;
|
|
494
452
|
|
|
495
453
|
/**
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
* event handler and (via re-export) by the offline benchmark, so the two can
|
|
499
|
-
* never drift apart.
|
|
500
|
-
*
|
|
501
|
-
* Walk backwards from the end of `messages`, skipping any message that is
|
|
502
|
-
* neither `user`, `assistant`, nor `toolResult` (e.g. system-role bookkeeping
|
|
503
|
-
* entries). The first such message found determines the state:
|
|
504
|
-
* - `user` (a plain user turn message) => "idle": this call is the FIRST LLM
|
|
505
|
-
* call of a turn, i.e. the decision is being made from a turn-start state.
|
|
506
|
-
* - `assistant` or `toolResult` (mid-chain) => "mid_loop": the agent is
|
|
507
|
-
* iterating (already made at least one call this turn, or is reacting to a
|
|
508
|
-
* tool result).
|
|
509
|
-
* - No such message found (empty history, or system-only) => "idle" (safe
|
|
510
|
-
* default: nothing suggests an in-progress loop).
|
|
511
|
-
*
|
|
512
|
-
* The benchmark (scripts/benchmark.mjs) reclassifies its candidate labels to
|
|
513
|
-
* this SAME definition, reusing this function directly, so the
|
|
514
|
-
* defaults below rest on evidence measured with the exact definition the
|
|
515
|
-
* runtime observes. (An earlier turn-end-based benchmark definition -- "idle"
|
|
516
|
-
* meant "this IS the turn's last assistant message", only knowable in
|
|
517
|
-
* hindsight -- has been retired: the runtime cannot know in advance whether a
|
|
518
|
-
* call is a turn's last one, so that definition was never runtime-observable.)
|
|
454
|
+
* Shared runtime/benchmark state classifier; see README.md Configuration and
|
|
455
|
+
* docs/v2-design.md §4 for the turn-start definition and retired turn-end analysis.
|
|
519
456
|
*/
|
|
520
457
|
export function classifyAgentStateFromMessages(messages: MinimalMessage[]): AgentState {
|
|
521
458
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -677,7 +614,7 @@ export function isWithinRecencyWindow(index: number, boundaryIndex: number): boo
|
|
|
677
614
|
* Used by the error-input purge strategy's "older than N turns" rule; a
|
|
678
615
|
* message at the very end of the conversation has 0 turns elapsed.
|
|
679
616
|
*/
|
|
680
|
-
|
|
617
|
+
function computeTurnsElapsedSince(messages: MinimalMessage[], index: number): number {
|
|
681
618
|
let count = 0;
|
|
682
619
|
for (let i = index + 1; i < messages.length; i++) {
|
|
683
620
|
if (messages[i].role === "user") count++;
|
|
@@ -974,7 +911,7 @@ export function resolvePruneTombstoneState(entries: MinimalSessionEntry[]): Prun
|
|
|
974
911
|
return { activeDecisions, activeIdempotencyKeys, restoredKeys, lastDecisionByKey };
|
|
975
912
|
}
|
|
976
913
|
|
|
977
|
-
|
|
914
|
+
function buildPlaceholderText(reason: string, charsRemoved: number): string {
|
|
978
915
|
return `[pruned by ${EXTENSION_ID}: ${reason} (${charsRemoved} chars removed)]`;
|
|
979
916
|
}
|
|
980
917
|
|
|
@@ -1581,7 +1518,7 @@ export function collectCompletedToolCallOccurrences(
|
|
|
1581
1518
|
}
|
|
1582
1519
|
|
|
1583
1520
|
/** Placeholder text for a duplicate tool result whose content was replaced. */
|
|
1584
|
-
|
|
1521
|
+
function buildDedupePlaceholder(toolName: string, newestToolCallId: string): string {
|
|
1585
1522
|
return `[pruned by ${EXTENSION_ID}: duplicate ${toolName} call result; see newest occurrence (call ${newestToolCallId})]`;
|
|
1586
1523
|
}
|
|
1587
1524
|
|
|
@@ -1642,46 +1579,6 @@ export const errorPurgeStrategy: PruneStrategy = {
|
|
|
1642
1579
|
},
|
|
1643
1580
|
};
|
|
1644
1581
|
|
|
1645
|
-
// When the same file path is read and/or written multiple times in a
|
|
1646
|
-
// session, older read/write/edit tool *outputs* for that path can become
|
|
1647
|
-
// stale relative to a later operation. This strategy proposes replacing
|
|
1648
|
-
// those older outputs with a placeholder, per the following CONSERVATIVE,
|
|
1649
|
-
// binding rules (do not loosen without an architect decision):
|
|
1650
|
-
//
|
|
1651
|
-
// 1. A full-file read (no offset/limit args) is superseded by a LATER
|
|
1652
|
-
// full-file read of the same normalized path.
|
|
1653
|
-
// 2. A partial read (offset/limit args present) is NOT superseded by a
|
|
1654
|
-
// later partial read UNLESS the later read's range fully covers the
|
|
1655
|
-
// earlier one's range. If range coverage can't be reliably determined
|
|
1656
|
-
// from the args (e.g. earlier read has no limit — reads to EOF — but
|
|
1657
|
-
// the later read is bounded), skip: do not propose supersession.
|
|
1658
|
-
// Full-file reads are represented as the range [1, EOF); comparing
|
|
1659
|
-
// that representation via the same coverage check naturally satisfies
|
|
1660
|
-
// rule 1 as a special case of rule 2 (see `readRangeCovers`).
|
|
1661
|
-
// 3. ANY read of a path (full or partial) is superseded once a LATER
|
|
1662
|
-
// *successful* (non-error) write/edit to that same path occurs — the
|
|
1663
|
-
// old read output is now known-stale regardless of read-vs-read range
|
|
1664
|
-
// comparisons. The placeholder explicitly notes the file has since
|
|
1665
|
-
// changed. An ERRORED later write/edit does NOT count (the file may be
|
|
1666
|
-
// unchanged; conservative).
|
|
1667
|
-
// 4. Older write/edit outputs for a path are superseded by a LATER
|
|
1668
|
-
// *successful* write/edit to that same path (again, a later errored
|
|
1669
|
-
// write/edit never supersedes). Write/edit tool outputs are usually
|
|
1670
|
-
// small, so proposals are only ever emitted when there is a non-empty
|
|
1671
|
-
// result to prune; the shared net-benefit gate additionally decides
|
|
1672
|
-
// whether the estimated savings are worth the cache-bust cost.
|
|
1673
|
-
//
|
|
1674
|
-
// The NEWEST operation for a given path (by message order) is never
|
|
1675
|
-
// superseded/pruned, regardless of kind — there is nothing later to point
|
|
1676
|
-
// the placeholder at.
|
|
1677
|
-
//
|
|
1678
|
-
// Path extraction is defensive: pi's built-in `read`/`write`/`edit` tools
|
|
1679
|
-
// all accept a `path` argument (see packages/coding-agent/src/core/tools/
|
|
1680
|
-
// {read,write,edit}.ts); some tool-call renderers also fall back to a
|
|
1681
|
-
// legacy `file_path` name, and `filePath` is accepted as a further
|
|
1682
|
-
// defensive fallback. `bash` is explicitly OUT of scope: this strategy does
|
|
1683
|
-
// not attempt to parse shell commands for file paths.
|
|
1684
|
-
|
|
1685
1582
|
const SUPERSEDED_FILE_OPS_STRATEGY_ID = "superseded-file-ops";
|
|
1686
1583
|
|
|
1687
1584
|
type FileOpKind = "read" | "write" | "edit";
|
|
@@ -1736,11 +1633,6 @@ export function computeFileReadRange(args: Record<string, unknown> | undefined):
|
|
|
1736
1633
|
return { start, end };
|
|
1737
1634
|
}
|
|
1738
1635
|
|
|
1739
|
-
/** True when a range represents a full-file read (starts at line 1, no limit). */
|
|
1740
|
-
export function isFullFileReadRange(range: FileReadRange): boolean {
|
|
1741
|
-
return range.start === 1 && range.end === undefined;
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
1636
|
/**
|
|
1745
1637
|
* Conservative range-coverage check: does `later` fully cover `earlier`?
|
|
1746
1638
|
* Implements rules 1+2 above via one unified representation (a full-file
|
|
@@ -2234,7 +2126,7 @@ export function sessionEntriesToMessages(
|
|
|
2234
2126
|
entries: MinimalSessionEntry[],
|
|
2235
2127
|
projection?: MinimalSessionProjection,
|
|
2236
2128
|
): MinimalMessage[] {
|
|
2237
|
-
if (projection) return
|
|
2129
|
+
if (projection) return projection.entries.flatMap((entry) => entry.messages);
|
|
2238
2130
|
const messages: MinimalMessage[] = [];
|
|
2239
2131
|
for (const entry of entries) {
|
|
2240
2132
|
if (entry.type === "message" && entry.message) messages.push(entry.message);
|
|
@@ -2373,11 +2265,9 @@ const STRATEGY_ALIASES: Record<string, StrategyKey> = {
|
|
|
2373
2265
|
"error-purge": "errorPurge",
|
|
2374
2266
|
errorpurge: "errorPurge",
|
|
2375
2267
|
error_purge: "errorPurge",
|
|
2376
|
-
errorPurge: "errorPurge",
|
|
2377
2268
|
"superseded-file-ops": "supersededFileOps",
|
|
2378
2269
|
supersededfileops: "supersededFileOps",
|
|
2379
2270
|
superseded_file_ops: "supersededFileOps",
|
|
2380
|
-
supersededFileOps: "supersededFileOps",
|
|
2381
2271
|
};
|
|
2382
2272
|
|
|
2383
2273
|
const STRATEGY_DISPLAY_NAMES: Record<StrategyKey, string> = {
|
|
@@ -2420,9 +2310,11 @@ export function parseContextPruningArgs(rawArgs: string): ContextPruningSubcomma
|
|
|
2420
2310
|
return { kind: "toggle" };
|
|
2421
2311
|
case "strategy": {
|
|
2422
2312
|
const [nameRaw, valueRaw] = rest;
|
|
2423
|
-
const
|
|
2424
|
-
|
|
2425
|
-
|
|
2313
|
+
const normalizedName = nameRaw?.toLowerCase();
|
|
2314
|
+
const key =
|
|
2315
|
+
normalizedName && Object.hasOwn(STRATEGY_ALIASES, normalizedName)
|
|
2316
|
+
? STRATEGY_ALIASES[normalizedName]
|
|
2317
|
+
: undefined;
|
|
2426
2318
|
const value = valueRaw?.toLowerCase();
|
|
2427
2319
|
if (!key || (value !== "on" && value !== "off")) return { kind: "unknown", raw: rawArgs };
|
|
2428
2320
|
return { kind: "strategy", strategy: key, value: value === "on" };
|
|
@@ -2568,7 +2460,6 @@ function notify(
|
|
|
2568
2460
|
}
|
|
2569
2461
|
|
|
2570
2462
|
export const __testing = {
|
|
2571
|
-
globToRegExp,
|
|
2572
2463
|
getConfigPath,
|
|
2573
2464
|
readConfig,
|
|
2574
2465
|
writeConfig,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A pi extension that provides `/fast` and `/ultrafast` toggles for supported direct OpenAI API, OpenAI Codex, and Anthropic Claude models. It selects the provider-specific Fast-mode request shape from the currently selected model.
|
|
4
4
|
|
|
5
|
-
For eligible direct OpenAI API models it injects:
|
|
5
|
+
For eligible direct OpenAI API-key models it injects:
|
|
6
6
|
|
|
7
7
|
```json
|
|
8
8
|
{
|
|
@@ -10,6 +10,14 @@ For eligible direct OpenAI API models it injects:
|
|
|
10
10
|
}
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
For eligible direct OpenAI ChatGPT OAuth models it injects:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"service_tier": "priority"
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
13
21
|
For eligible OpenAI Codex OAuth models it injects:
|
|
14
22
|
|
|
15
23
|
```json
|
|
@@ -39,16 +47,15 @@ Direct OpenAI API Fast mode requires all of the following:
|
|
|
39
47
|
- Provider `openai`.
|
|
40
48
|
- API `openai-responses` or `openai-completions`.
|
|
41
49
|
- Model `gpt-6.1-sol`, `gpt-6-astra`, `gpt-6-sol`, or `gpt-6-luna`.
|
|
42
|
-
- API-key auth
|
|
50
|
+
- API-key auth sets `service_tier: "fast"`; Pi's `openai` ChatGPT OAuth sets `priority`.
|
|
43
51
|
- No existing `service_tier` field in the request payload.
|
|
44
52
|
|
|
45
|
-
The direct OpenAI route stays on OpenAI's direct API contract
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
and request payloads, not live or paid provider calls.
|
|
53
|
+
The direct OpenAI route stays on OpenAI's direct API contract but selects the tier by auth
|
|
54
|
+
type. OAuth uses `priority` because its endpoint rejected `fast`; acceptance or application
|
|
55
|
+
of `priority` on Pi's OAuth endpoint has not been live-verified. This direct route is never
|
|
56
|
+
forwarded to legacy `openai-codex` or its subscription-usage endpoint. The request shape is
|
|
57
|
+
support evidence, not certification; tests use mocked eligibility and payloads only, with
|
|
58
|
+
no live or paid provider calls.
|
|
52
59
|
|
|
53
60
|
### Ultrafast support (initial, fail-closed)
|
|
54
61
|
|
package/extensions/fast/index.ts
CHANGED
|
@@ -155,7 +155,7 @@ function describeMode(state: SessionState): string {
|
|
|
155
155
|
return state.config.enabled ? "on (config default)" : "off (config default)";
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
function getEligibility(ctx: ExtensionContext): Eligibility {
|
|
158
|
+
function getEligibility(ctx: ExtensionContext, includeServiceTier = true): Eligibility {
|
|
159
159
|
const model = ctx.model;
|
|
160
160
|
if (!model) {
|
|
161
161
|
return { eligible: false, modelKey: "no-model", reason: "no model is selected" };
|
|
@@ -237,7 +237,11 @@ function getEligibility(ctx: ExtensionContext): Eligibility {
|
|
|
237
237
|
eligible: true,
|
|
238
238
|
modelKey: key,
|
|
239
239
|
provider: "openai",
|
|
240
|
-
serviceTier:
|
|
240
|
+
serviceTier: includeServiceTier
|
|
241
|
+
? ctx.modelRegistry.isUsingOAuth(model)
|
|
242
|
+
? OPENAI_FAST_SERVICE_TIER
|
|
243
|
+
: OPENAI_API_FAST_SERVICE_TIER
|
|
244
|
+
: undefined,
|
|
241
245
|
};
|
|
242
246
|
}
|
|
243
247
|
|
|
@@ -328,7 +332,7 @@ function updateStatus(ctx: ExtensionContext, state: SessionState): void {
|
|
|
328
332
|
return;
|
|
329
333
|
}
|
|
330
334
|
|
|
331
|
-
const eligibility = getEligibility(ctx);
|
|
335
|
+
const eligibility = getEligibility(ctx, false);
|
|
332
336
|
ctx.ui.setStatus(EXTENSION_ID, isFastEnabled(state) && eligibility.eligible ? "fast" : undefined);
|
|
333
337
|
}
|
|
334
338
|
|
|
@@ -346,7 +350,7 @@ function getStatusMessage(ctx: ExtensionContext, state: SessionState): string {
|
|
|
346
350
|
}
|
|
347
351
|
|
|
348
352
|
const enabled = isFastEnabled(state);
|
|
349
|
-
const eligibility = getEligibility(ctx);
|
|
353
|
+
const eligibility = getEligibility(ctx, enabled);
|
|
350
354
|
const active = enabled && eligibility.eligible;
|
|
351
355
|
|
|
352
356
|
if (active) {
|
|
@@ -405,7 +409,7 @@ async function injectAnthropicFastHeader(
|
|
|
405
409
|
hasActiveTools: boolean,
|
|
406
410
|
): Promise<void> {
|
|
407
411
|
if (state.ultrafast || !isFastEnabled(state)) return;
|
|
408
|
-
const eligibility = getEligibility(ctx);
|
|
412
|
+
const eligibility = getEligibility(ctx, false);
|
|
409
413
|
if (!eligibility.eligible || eligibility.provider !== "anthropic") return;
|
|
410
414
|
|
|
411
415
|
const matchingKeys = Object.keys(headers).filter(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-fast",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "A pi extension that enables Fast mode for supported direct OpenAI API, OpenAI Codex, and Anthropic Claude models based on the currently selected model.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -13,10 +13,6 @@ type GitStatusSnapshot = {
|
|
|
13
13
|
|
|
14
14
|
type PullRequestSnapshot = {
|
|
15
15
|
number?: number | string;
|
|
16
|
-
state?: string;
|
|
17
|
-
isDraft?: boolean;
|
|
18
|
-
url?: string;
|
|
19
|
-
title?: string;
|
|
20
16
|
};
|
|
21
17
|
|
|
22
18
|
type CommandResult = {
|
|
@@ -74,7 +70,7 @@ const GIT_STATUS_ARGS = [
|
|
|
74
70
|
"--porcelain=v2",
|
|
75
71
|
"--branch",
|
|
76
72
|
] as const;
|
|
77
|
-
const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number
|
|
73
|
+
const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number"] as const;
|
|
78
74
|
|
|
79
75
|
function createEmptyGitStatus(): GitStatusSnapshot {
|
|
80
76
|
return {
|
|
@@ -202,10 +198,6 @@ function parsePullRequestJson(stdout: string): PullRequestSnapshot | undefined {
|
|
|
202
198
|
if (typeof record.number === "number" || typeof record.number === "string") {
|
|
203
199
|
snapshot.number = record.number;
|
|
204
200
|
}
|
|
205
|
-
if (typeof record.state === "string") snapshot.state = record.state;
|
|
206
|
-
if (typeof record.isDraft === "boolean") snapshot.isDraft = record.isDraft;
|
|
207
|
-
if (typeof record.url === "string") snapshot.url = record.url;
|
|
208
|
-
if (typeof record.title === "string") snapshot.title = record.title;
|
|
209
201
|
return snapshot;
|
|
210
202
|
}
|
|
211
203
|
|
|
@@ -228,13 +220,7 @@ function pullRequestSnapshotsEqual(
|
|
|
228
220
|
left: PullRequestSnapshot | undefined,
|
|
229
221
|
right: PullRequestSnapshot | undefined,
|
|
230
222
|
): boolean {
|
|
231
|
-
return
|
|
232
|
-
left?.number === right?.number &&
|
|
233
|
-
left?.state === right?.state &&
|
|
234
|
-
left?.isDraft === right?.isDraft &&
|
|
235
|
-
left?.url === right?.url &&
|
|
236
|
-
left?.title === right?.title
|
|
237
|
-
);
|
|
223
|
+
return left?.number === right?.number;
|
|
238
224
|
}
|
|
239
225
|
|
|
240
226
|
function defaultRunner(
|
|
@@ -766,11 +766,6 @@ async function writeLibrarianPreferences(preferences: LibrarianPreferences): Pro
|
|
|
766
766
|
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
767
767
|
}
|
|
768
768
|
|
|
769
|
-
async function writeCachePreference(preference: CacheMode): Promise<void> {
|
|
770
|
-
const preferences = await readLibrarianPreferences();
|
|
771
|
-
await writeLibrarianPreferences({ ...preferences, cacheMode: preference });
|
|
772
|
-
}
|
|
773
|
-
|
|
774
769
|
function resolveCacheDecision(preference: CacheMode): { enabled: boolean; reason: string } {
|
|
775
770
|
if (preference === "enabled") {
|
|
776
771
|
return { enabled: true, reason: "cache preference enabled; using cached local checkouts" };
|
|
@@ -1045,7 +1040,7 @@ function tryCurlyQuotePathVariant(filePath: string): string {
|
|
|
1045
1040
|
}
|
|
1046
1041
|
/**
|
|
1047
1042
|
* Returns all read path candidates including Pi's resolveReadPathAsync fallback
|
|
1048
|
-
* variants.
|
|
1043
|
+
* variants.
|
|
1049
1044
|
*/
|
|
1050
1045
|
function getReadPathCandidates(resolved: string): string[] {
|
|
1051
1046
|
const nfd = tryNFDPathVariant(resolved);
|
|
@@ -1058,11 +1053,6 @@ function getReadPathCandidates(resolved: string): string[] {
|
|
|
1058
1053
|
];
|
|
1059
1054
|
}
|
|
1060
1055
|
|
|
1061
|
-
function resolveToolPath(cwd: string, rawPath: string): string {
|
|
1062
|
-
const normalized = rawPath.startsWith("@") ? rawPath.slice(1) : rawPath;
|
|
1063
|
-
return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(cwd, normalized);
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
1056
|
function getBlockedBashReason(
|
|
1067
1057
|
command: string,
|
|
1068
1058
|
options: { workspace: string; cacheRoot: string; cacheEnabled: boolean },
|
|
@@ -1165,17 +1155,9 @@ function createLibrarianRuntimeGuardExtension(options: {
|
|
|
1165
1155
|
(options.cacheEnabled && (isInside(options.cacheRoot, p) || isInside(realCacheRoot, p)))
|
|
1166
1156
|
);
|
|
1167
1157
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
return {
|
|
1172
|
-
block: true,
|
|
1173
|
-
reason: `Librarian read is limited to its workspace/cache: ${realPath}`,
|
|
1174
|
-
};
|
|
1175
|
-
// Pi-effective path check (monotonic: block if either resolution is
|
|
1176
|
-
// outside). For the read tool, also check Pi's resolveReadPathAsync
|
|
1177
|
-
// fallback variants so a missing file whose variant resolves outside is
|
|
1178
|
-
// not silently allowed.
|
|
1158
|
+
// For the read tool, also check Pi's resolveReadPathAsync fallback
|
|
1159
|
+
// variants so a missing file whose variant resolves outside is not
|
|
1160
|
+
// silently allowed.
|
|
1179
1161
|
try {
|
|
1180
1162
|
const piResolved = resolveToCwd(input.path, options.workspace);
|
|
1181
1163
|
const candidates = getReadPathCandidates(piResolved);
|
|
@@ -1293,12 +1275,6 @@ function addAssistantMessageUsage(total: Usage, message: unknown): void {
|
|
|
1293
1275
|
addUsage(total, assistant.usage as Usage);
|
|
1294
1276
|
}
|
|
1295
1277
|
|
|
1296
|
-
function aggregateAssistantUsage(messages: unknown[]): Usage {
|
|
1297
|
-
const total = createEmptyUsage();
|
|
1298
|
-
for (const message of messages) addAssistantMessageUsage(total, message);
|
|
1299
|
-
return total;
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
1278
|
function addSessionEventUsage(total: Usage, event: unknown): void {
|
|
1303
1279
|
const sessionEvent = event as { type?: string; message?: unknown; result?: { usage?: Usage } };
|
|
1304
1280
|
if (sessionEvent?.type === "message_end")
|
|
@@ -1481,8 +1457,7 @@ export const __test__ = {
|
|
|
1481
1457
|
isModelAvailabilityError,
|
|
1482
1458
|
parseModelPreference,
|
|
1483
1459
|
resolveThinkingLevel,
|
|
1484
|
-
|
|
1485
|
-
addAssistantMessageUsage,
|
|
1460
|
+
createEmptyUsage,
|
|
1486
1461
|
addSessionEventUsage,
|
|
1487
1462
|
inspectFinalAssistant,
|
|
1488
1463
|
classifyRunFailure,
|
|
@@ -1490,14 +1465,12 @@ export const __test__ = {
|
|
|
1490
1465
|
createLibrarianAbortController,
|
|
1491
1466
|
cleanupExpiredCache,
|
|
1492
1467
|
cleanupExpiredRepos,
|
|
1493
|
-
cleanupLockOwnershipError,
|
|
1494
1468
|
tryAcquireCleanupLock,
|
|
1495
1469
|
heartbeatCleanupLock,
|
|
1496
1470
|
releaseCleanupLock,
|
|
1497
1471
|
CLEANUP_LOCK_FILE,
|
|
1498
1472
|
CLEANUP_LOCK_STALE_MS,
|
|
1499
1473
|
CLEANUP_LOCK_MAX_LEASE_MS,
|
|
1500
|
-
isCleanupLockLeaseExpired,
|
|
1501
1474
|
};
|
|
1502
1475
|
|
|
1503
1476
|
export default function librarianExtension(pi: ExtensionAPI) {
|
|
@@ -1668,17 +1641,6 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1668
1641
|
const cacheRoot = getUserCacheReposRoot();
|
|
1669
1642
|
const configPath = getCacheConfigPath();
|
|
1670
1643
|
|
|
1671
|
-
const setPreference = async (mode: CacheMode): Promise<string | undefined> => {
|
|
1672
|
-
cachePreference = mode;
|
|
1673
|
-
try {
|
|
1674
|
-
await writeCachePreference(mode);
|
|
1675
|
-
return undefined;
|
|
1676
|
-
} catch (error) {
|
|
1677
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1678
|
-
return `Preference changed for this process, but could not save ${configPath}: ${message}`;
|
|
1679
|
-
}
|
|
1680
|
-
};
|
|
1681
|
-
|
|
1682
1644
|
const formatSetMessage = (mode: CacheMode, warning?: string): string => {
|
|
1683
1645
|
const main =
|
|
1684
1646
|
mode === "enabled"
|
|
@@ -1688,20 +1650,20 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1688
1650
|
};
|
|
1689
1651
|
|
|
1690
1652
|
if (action === "on" || action === "enable") {
|
|
1691
|
-
const warning = await
|
|
1653
|
+
const warning = await savePreferences({ ...currentPreferences(), cacheMode: "enabled" });
|
|
1692
1654
|
notifyCommand(ctx, formatSetMessage("enabled", warning), warning ? "warning" : "info");
|
|
1693
1655
|
return;
|
|
1694
1656
|
}
|
|
1695
1657
|
|
|
1696
1658
|
if (action === "off" || action === "disable") {
|
|
1697
|
-
const warning = await
|
|
1659
|
+
const warning = await savePreferences({ ...currentPreferences(), cacheMode: "disabled" });
|
|
1698
1660
|
notifyCommand(ctx, formatSetMessage("disabled", warning), warning ? "warning" : "info");
|
|
1699
1661
|
return;
|
|
1700
1662
|
}
|
|
1701
1663
|
|
|
1702
1664
|
if (action === "toggle") {
|
|
1703
1665
|
const next = cachePreference === "enabled" ? "disabled" : "enabled";
|
|
1704
|
-
const warning = await
|
|
1666
|
+
const warning = await savePreferences({ ...currentPreferences(), cacheMode: next });
|
|
1705
1667
|
notifyCommand(ctx, formatSetMessage(next, warning), warning ? "warning" : "info");
|
|
1706
1668
|
return;
|
|
1707
1669
|
}
|
|
@@ -10,7 +10,7 @@ It replaces pi's built-in footer with a cleaner two-line layout that focuses on
|
|
|
10
10
|
- TLH-style git dirty counts, ahead/behind counts, and optional PR number
|
|
11
11
|
- current repo name
|
|
12
12
|
- current context percentage
|
|
13
|
-
- red `DUMB ZONE` indicator when context usage is above
|
|
13
|
+
- red `DUMB ZONE` indicator when context usage is above 350k tokens
|
|
14
14
|
- current model and thinking level
|
|
15
15
|
- OpenAI Codex usage windows, labeled from the durations reported by OpenAI
|
|
16
16
|
- `xp` marker when Pi experimental features are enabled
|
|
@@ -39,7 +39,7 @@ main · +1 ~2 ?3 ↑1 • PR #42
|
|
|
39
39
|
|
|
40
40
|
The markers are `!` conflicts, `+` staged, `~` unstaged, `?` untracked, `↑` ahead, and `↓` behind. PR numbers are fetched best-effort with `gh pr view`. Git polling is skipped until Pi reports that the project is trusted, and `git status` is run with fsmonitor disabled.
|
|
41
41
|
|
|
42
|
-
When context usage is above
|
|
42
|
+
When context usage is above 350k tokens, the bottom-left line includes a red warning:
|
|
43
43
|
|
|
44
44
|
```text
|
|
45
45
|
44.1% · DUMB ZONE
|
|
@@ -115,7 +115,7 @@ Example:
|
|
|
115
115
|
"showPercent": true,
|
|
116
116
|
"dumbZone": {
|
|
117
117
|
"enabled": true,
|
|
118
|
-
"thresholdTokens":
|
|
118
|
+
"thresholdTokens": 350000,
|
|
119
119
|
"label": "DUMB ZONE",
|
|
120
120
|
"color": "error"
|
|
121
121
|
}
|
|
@@ -231,7 +231,7 @@ Disable git dirty/ahead/PR status:
|
|
|
231
231
|
|
|
232
232
|
- **Top left:** current git branch plus dirty/ahead/PR status when available
|
|
233
233
|
- **Top right:** current repo directory name
|
|
234
|
-
- **Bottom left:** current context usage percentage, plus red `DUMB ZONE` above
|
|
234
|
+
- **Bottom left:** current context usage percentage, plus red `DUMB ZONE` above 350k context tokens
|
|
235
235
|
- **Bottom left on `openai-codex`:** current context usage percentage plus the Codex usage windows reported for the account
|
|
236
236
|
- **Bottom left on `openai` ChatGPT OAuth:** current context usage percentage plus `usage unsupported`; no legacy WHAM request is attempted
|
|
237
237
|
- **Bottom left with `PI_EXPERIMENTAL=1`:** current context usage percentage plus `xp`
|
|
@@ -246,7 +246,7 @@ This extension also lives inside the broader [`pi-extensions`](../../README.md)
|
|
|
246
246
|
- Replaces pi's built-in footer entirely.
|
|
247
247
|
- Uses pi footer data for git branch updates, plus background cached git/gh checks for dirty counts and PR number after project trust is granted.
|
|
248
248
|
- Shows only context percentage, not context window size.
|
|
249
|
-
- Shows `DUMB ZONE` only while context usage is above
|
|
249
|
+
- Shows `DUMB ZONE` only while context usage is above 350k tokens.
|
|
250
250
|
- Shows the model id rather than a provider-specific display label.
|
|
251
251
|
- Shows `xp` when `PI_EXPERIMENTAL=1`.
|
|
252
252
|
- For `openai-codex`, reads pi's stored OAuth login and fetches usage from ChatGPT's backend usage endpoint. Window labels are inferred from each window's reported duration; missing or unrecognized durations use neutral `usage` labels rather than guessing from window position.
|
|
@@ -24,7 +24,7 @@ const DEFAULT_CONFIG: MinimalFooterConfig = {
|
|
|
24
24
|
showPercent: true,
|
|
25
25
|
dumbZone: {
|
|
26
26
|
enabled: true,
|
|
27
|
-
thresholdTokens:
|
|
27
|
+
thresholdTokens: 350_000,
|
|
28
28
|
label: "DUMB ZONE",
|
|
29
29
|
color: "error",
|
|
30
30
|
},
|
|
@@ -148,10 +148,6 @@ type GitStatusSnapshot = {
|
|
|
148
148
|
|
|
149
149
|
type PullRequestSnapshot = {
|
|
150
150
|
number?: number | string;
|
|
151
|
-
state?: string;
|
|
152
|
-
isDraft?: boolean;
|
|
153
|
-
url?: string;
|
|
154
|
-
title?: string;
|
|
155
151
|
};
|
|
156
152
|
|
|
157
153
|
type CommandResult = {
|
|
@@ -200,7 +196,7 @@ const GIT_STATUS_ARGS = [
|
|
|
200
196
|
"--porcelain=v2",
|
|
201
197
|
"--branch",
|
|
202
198
|
] as const;
|
|
203
|
-
const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number
|
|
199
|
+
const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number"] as const;
|
|
204
200
|
|
|
205
201
|
function readConfigFile(path: string): RecursivePartial<MinimalFooterConfig> {
|
|
206
202
|
if (!existsSync(path)) return {};
|
|
@@ -468,10 +464,6 @@ function parsePullRequestJson(stdout: string): PullRequestSnapshot | undefined {
|
|
|
468
464
|
if (typeof record.number === "number" || typeof record.number === "string") {
|
|
469
465
|
snapshot.number = record.number;
|
|
470
466
|
}
|
|
471
|
-
if (typeof record.state === "string") snapshot.state = record.state;
|
|
472
|
-
if (typeof record.isDraft === "boolean") snapshot.isDraft = record.isDraft;
|
|
473
|
-
if (typeof record.url === "string") snapshot.url = record.url;
|
|
474
|
-
if (typeof record.title === "string") snapshot.title = record.title;
|
|
475
467
|
return snapshot;
|
|
476
468
|
}
|
|
477
469
|
|
|
@@ -494,13 +486,7 @@ function pullRequestSnapshotsEqual(
|
|
|
494
486
|
left: PullRequestSnapshot | undefined,
|
|
495
487
|
right: PullRequestSnapshot | undefined,
|
|
496
488
|
): boolean {
|
|
497
|
-
return
|
|
498
|
-
left?.number === right?.number &&
|
|
499
|
-
left?.state === right?.state &&
|
|
500
|
-
left?.isDraft === right?.isDraft &&
|
|
501
|
-
left?.url === right?.url &&
|
|
502
|
-
left?.title === right?.title
|
|
503
|
-
);
|
|
489
|
+
return left?.number === right?.number;
|
|
504
490
|
}
|
|
505
491
|
|
|
506
492
|
function defaultRunner(
|