@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andares/pi-toolkits",
3
- "version": "0.11.2",
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
- * `shouldDeferViewportInputToOverlay()`, so the keystroke opens transcript
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.3: keeps a persisted default usable when cycling inside a
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.3).
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.3: only persist the default model when the caller asked for it.
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.3's prototype shape
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 =
@@ -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.3
47
- // model-selector.js); everything else on ModelRuntime is never reached
48
- // from this component.
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