@andares/pi-toolkits 0.11.2 → 0.11.3
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/package.json +1 -1
- package/src/features/compact/compact.test.ts +55 -0
- package/src/features/favorites/constants.ts +2 -1
- package/src/features/favorites/favorites.test.ts +36 -0
- package/src/features/favorites/patch-cycle.ts +3 -3
- package/src/features/favorites/patch-selector.ts +1 -1
- package/src/lib/model-picker.ts +4 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andares/pi-toolkits",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"description": "Integrated toolkit extension for pi — ask mode (read-only Q&A), session-scoped permission checks, model favorites, prompt stash, third-party code review, session autostart, a dedicated compaction model, and an agent memory framework (/ask-init)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -525,6 +525,61 @@ describe("session_before_compact handler", () => {
|
|
|
525
525
|
expect(compaction.usage?.input).toBe(107);
|
|
526
526
|
});
|
|
527
527
|
|
|
528
|
+
it("keeps the cost provider-reported only when both split-turn parts report it", async () => {
|
|
529
|
+
// 1.5.0's combineUsage rule end-to-end: the merged compaction usage, not
|
|
530
|
+
// just the helper. A part that reports no provider cost is enough to make
|
|
531
|
+
// the sum "not provider-reported"; only both parts reporting it flips it
|
|
532
|
+
// to true (asserted below on a second run). The flag is cost provenance,
|
|
533
|
+
// not the token sum.
|
|
534
|
+
getCompactStore().setModel("google/gemini-2.5-flash");
|
|
535
|
+
const { handler } = registeredHandler();
|
|
536
|
+
const withFlag = (flag: boolean | undefined) => {
|
|
537
|
+
const usage = fakeUsage();
|
|
538
|
+
if (flag !== undefined) usage.cost.totalFromProvider = flag;
|
|
539
|
+
return usage;
|
|
540
|
+
};
|
|
541
|
+
const { ctx } = fakeCtx({
|
|
542
|
+
complete: vi.fn(async (_model, context) => {
|
|
543
|
+
const text = (context.messages[0]?.content as Array<{ text?: string }>)[0]
|
|
544
|
+
?.text;
|
|
545
|
+
return text?.includes("# Instructions")
|
|
546
|
+
? fakeResponse("TURN PREFIX", { usage: withFlag(true) })
|
|
547
|
+
: fakeResponse("HISTORY", { usage: withFlag(undefined) });
|
|
548
|
+
}),
|
|
549
|
+
});
|
|
550
|
+
const event = fakeEvent({
|
|
551
|
+
turnPrefixMessages: [userMessage("prefix msg")],
|
|
552
|
+
isSplitTurn: true,
|
|
553
|
+
});
|
|
554
|
+
const result = (await handler(
|
|
555
|
+
event as never,
|
|
556
|
+
ctx,
|
|
557
|
+
)) as SessionBeforeCompactResult | undefined;
|
|
558
|
+
// Read through the structural view, not the host's `CompleteUsage`:
|
|
559
|
+
// `cost.totalFromProvider` only exists on pi ≥ 1.5.0's types, so reading
|
|
560
|
+
// it directly from the host type breaks typecheck on the 1.2–1.4 window
|
|
561
|
+
// even though the runtime field we write is the same.
|
|
562
|
+
const mergedUsage = result!.compaction!.usage as unknown as UsageLike;
|
|
563
|
+
expect(mergedUsage.cost.totalFromProvider).toBe(false);
|
|
564
|
+
|
|
565
|
+
// Same split turn, but both summaries report the cost themselves → true.
|
|
566
|
+
const both = fakeCtx({
|
|
567
|
+
complete: vi.fn(async (_model, context) => {
|
|
568
|
+
const text = (context.messages[0]?.content as Array<{ text?: string }>)[0]
|
|
569
|
+
?.text;
|
|
570
|
+
return text?.includes("# Instructions")
|
|
571
|
+
? fakeResponse("TURN PREFIX", { usage: withFlag(true) })
|
|
572
|
+
: fakeResponse("HISTORY", { usage: withFlag(true) });
|
|
573
|
+
}),
|
|
574
|
+
});
|
|
575
|
+
const bothResult = (await handler(
|
|
576
|
+
event as never,
|
|
577
|
+
both.ctx,
|
|
578
|
+
)) as SessionBeforeCompactResult | undefined;
|
|
579
|
+
const bothUsage = bothResult!.compaction!.usage as unknown as UsageLike;
|
|
580
|
+
expect(bothUsage.cost.totalFromProvider).toBe(true);
|
|
581
|
+
});
|
|
582
|
+
|
|
528
583
|
it("keeps the previous summary as history text on split turns with no messages", async () => {
|
|
529
584
|
getCompactStore().setModel("google/gemini-2.5-flash");
|
|
530
585
|
const { handler } = registeredHandler();
|
|
@@ -29,7 +29,8 @@ export const FAVORITES_FILE = "pi-toolkits-favorites.json";
|
|
|
29
29
|
* (0.87–0.99 defaulted to `regular`, where the binding is inert). On
|
|
30
30
|
* Windows/WSL `tui.altScreen.search` resolves to ctrl+f, and
|
|
31
31
|
* `TuiAltScreen.handleViewportInput()` checks it before
|
|
32
|
-
* `
|
|
32
|
+
* `shouldDeferViewportInput()` (named `shouldDeferViewportInputToOverlay()`
|
|
33
|
+
* through 1.4.0), so the keystroke opens transcript
|
|
33
34
|
* search and the selector never sees it (verified in pi-tui 1.5.0
|
|
34
35
|
* `dist/tui-alt-screen.js`: the search check is still the first key branch
|
|
35
36
|
* and still returns `{ consume: true }` unconditionally; the 1.5.0 changes to
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
setSharedFavoritesStore,
|
|
40
40
|
} from "./store.js";
|
|
41
41
|
import { applyCyclePatch } from "./patch-cycle.js";
|
|
42
|
+
import { styleFavorite } from "./constants.js";
|
|
42
43
|
import {
|
|
43
44
|
applyModelSelectorPatches,
|
|
44
45
|
setSelectorThemeProvider,
|
|
@@ -505,5 +506,40 @@ describe("selector patches", () => {
|
|
|
505
506
|
expect(renamedLine).toContain(
|
|
506
507
|
"(MiniMax M3 [rasmus@openrouter, parasail])",
|
|
507
508
|
);
|
|
509
|
+
// The full line pins the 1.5.0 column order (id → name → provider badge)
|
|
510
|
+
// and the favorite highlight not leaking ANSI codes into the name. The
|
|
511
|
+
// two spaces after → are the (empty) current-marker column; the fake
|
|
512
|
+
// selector has no currentModel, so no ✓ is drawn.
|
|
513
|
+
expect(redundantLine).toBe("→ claude-sonnet-4 [anthropic]");
|
|
514
|
+
expect(renamedLine).toBe(
|
|
515
|
+
" minimax/minimax-m3 (MiniMax M3 [rasmus@openrouter, parasail]) [openrouter]",
|
|
516
|
+
);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("keeps the favorite highlight while appending the disambiguating name", () => {
|
|
520
|
+
// Regression guard: the 1.5.0 name text must land AFTER the styled id
|
|
521
|
+
// (and outside its ANSI reset), or favorited ids would lose their color.
|
|
522
|
+
const favoriteModel = {
|
|
523
|
+
provider: "anthropic",
|
|
524
|
+
id: "claude-opus-4",
|
|
525
|
+
name: "Claude Opus 4 (max)",
|
|
526
|
+
};
|
|
527
|
+
getFavoritesStore().toggle(favoriteModel);
|
|
528
|
+
const lines: string[] = [];
|
|
529
|
+
const selector = fakeSelector({
|
|
530
|
+
activeModels: [],
|
|
531
|
+
filteredModels: [
|
|
532
|
+
{ provider: favoriteModel.provider, id: favoriteModel.id, model: favoriteModel },
|
|
533
|
+
] as unknown as FakeSelector["filteredModels"],
|
|
534
|
+
listContainer: {
|
|
535
|
+
clear() {},
|
|
536
|
+
addChild: (child: unknown) =>
|
|
537
|
+
lines.push((child as { text?: string }).text ?? ""),
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
selector.updateList();
|
|
541
|
+
expect(lines[0]).toBe(
|
|
542
|
+
`→ ${styleFavorite("claude-opus-4")} (Claude Opus 4 (max)) [anthropic]`,
|
|
543
|
+
);
|
|
508
544
|
});
|
|
509
545
|
});
|
|
@@ -59,7 +59,7 @@ interface CycleSessionInternals {
|
|
|
59
59
|
setDefaultModelAndProvider(provider: string, id: string): void;
|
|
60
60
|
};
|
|
61
61
|
/**
|
|
62
|
-
* pi 1.0–1.
|
|
62
|
+
* pi 1.0–1.5: keeps a persisted default usable when cycling inside a
|
|
63
63
|
* non-empty --models scope.
|
|
64
64
|
*/
|
|
65
65
|
_addPersistedDefaultToNonEmptyScope(model: unknown): void;
|
|
@@ -142,7 +142,7 @@ export function applyCyclePatch(): boolean {
|
|
|
142
142
|
return original.call(this, direction, options);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
// Same source and availability filtering as pi's stock cycling (1.0–1.
|
|
145
|
+
// Same source and availability filtering as pi's stock cycling (1.0–1.5).
|
|
146
146
|
const scoped = this._scopedModels.length > 0;
|
|
147
147
|
let candidates: CycleCandidate[];
|
|
148
148
|
if (scoped) {
|
|
@@ -187,7 +187,7 @@ export function applyCyclePatch(): boolean {
|
|
|
187
187
|
: this._getThinkingLevelForModelSwitch();
|
|
188
188
|
this.agent.state.model = next.model;
|
|
189
189
|
this.sessionManager.appendModelChange(next.model.provider, next.model.id);
|
|
190
|
-
// pi 1.0–1.
|
|
190
|
+
// pi 1.0–1.5: only persist the default model when the caller asked for it.
|
|
191
191
|
if (options.persist) {
|
|
192
192
|
this.settingsManager.setDefaultModelAndProvider(
|
|
193
193
|
next.model.provider,
|
|
@@ -149,7 +149,7 @@ let patched = false;
|
|
|
149
149
|
export function applyModelSelectorPatches(): boolean {
|
|
150
150
|
if (patched) return true;
|
|
151
151
|
// SAFETY: ModelSelectorComponent's members are TS-private (compile-time
|
|
152
|
-
// only); the structural view matches pi 1.0–1.
|
|
152
|
+
// only); the structural view matches pi 1.0–1.5's prototype shape
|
|
153
153
|
// (byte-identical across that span) and every touched method is
|
|
154
154
|
// existence-checked below before any patching happens.
|
|
155
155
|
const proto =
|
package/src/lib/model-picker.ts
CHANGED
|
@@ -43,9 +43,10 @@ function modelRuntimeFacade(ctx: ExtensionContext): ModelRuntime {
|
|
|
43
43
|
refresh: (options?: { signal?: AbortSignal }) =>
|
|
44
44
|
ctx.modelRegistry.refresh(options),
|
|
45
45
|
};
|
|
46
|
-
// SAFETY: the selector only touches the four members above (pi 1.0–1.
|
|
47
|
-
// model-selector.js
|
|
48
|
-
// from this
|
|
46
|
+
// SAFETY: the selector only touches the four members above (pi 1.0–1.5
|
|
47
|
+
// model-selector.js; the 1.5.0 rendering/page-key changes add no runtime
|
|
48
|
+
// call); everything else on ModelRuntime is never reached from this
|
|
49
|
+
// component.
|
|
49
50
|
return facade as unknown as ModelRuntime;
|
|
50
51
|
}
|
|
51
52
|
|