@andares/pi-toolkits 0.3.1 → 0.4.1

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.
@@ -34,13 +34,15 @@ import {
34
34
  import {
35
35
  createReviewRunner,
36
36
  pickReviewModel,
37
- resolveConfiguredModel,
38
37
  } from "./index.js";
38
+ import {
39
+ modelKey,
40
+ resolveConfiguredModel,
41
+ } from "../../lib/model-config.js";
39
42
  import { REVIEW_PROMPT } from "./prompt.js";
40
43
  import {
41
44
  ReviewStore,
42
45
  getReviewStore,
43
- modelKey,
44
46
  setSharedReviewStore,
45
47
  } from "./store.js";
46
48
 
@@ -283,17 +285,48 @@ describe("pickReviewModel", () => {
283
285
  expect(store.getModel()).toBe("anthropic/claude-opus-4");
284
286
  });
285
287
 
286
- it("shows the current config in the picker title", async () => {
288
+ it("puts the configured model first (Enter accepts it) and keeps config on cancel", async () => {
287
289
  const store = new ReviewStore(freshStoreFile());
288
- store.setModel("anthropic/claude-sonnet-4");
290
+ store.setModel("anthropic/claude-opus-4");
289
291
  const { ctx, select } = fakeCtx({ selectResult: undefined });
290
292
  await pickReviewModel(ctx, store);
291
293
  expect(select).toHaveBeenCalledWith(
292
- "选择评审模型 (当前: anthropic/claude-sonnet-4)",
293
- expect.any(Array),
294
+ "选择评审模型 (当前: anthropic/claude-opus-4,回车使用)",
295
+ [
296
+ "Claude Opus 4 (anthropic/claude-opus-4)",
297
+ "Claude Sonnet 4 (anthropic/claude-sonnet-4)",
298
+ "GPT-5 (openrouter/openai/gpt-5)",
299
+ ],
294
300
  );
295
301
  // Cancelling a change keeps the existing config.
296
- expect(store.getModel()).toBe("anthropic/claude-sonnet-4");
302
+ expect(store.getModel()).toBe("anthropic/claude-opus-4");
303
+ });
304
+
305
+ it("re-picking the configured model does not rewrite the store", async () => {
306
+ const store = new ReviewStore(freshStoreFile());
307
+ store.setModel("anthropic/claude-opus-4");
308
+ const spy = vi.spyOn(store, "setModel");
309
+ const { ctx } = fakeCtx({
310
+ selectResult: "Claude Opus 4 (anthropic/claude-opus-4)",
311
+ });
312
+ const picked = await pickReviewModel(ctx, store);
313
+ expect(picked).toBe(MODELS[1]);
314
+ expect(spy).not.toHaveBeenCalled();
315
+ });
316
+
317
+ it("persists and confirms when a different model is picked", async () => {
318
+ const store = new ReviewStore(freshStoreFile());
319
+ store.setModel("anthropic/claude-opus-4");
320
+ const { ctx, notify } = fakeCtx({
321
+ selectResult: "GPT-5 (openrouter/openai/gpt-5)",
322
+ });
323
+ const picked = await pickReviewModel(ctx, store);
324
+ expect(picked).toBe(MODELS[2]);
325
+ expect(store.getModel()).toBe("openrouter/openai/gpt-5");
326
+ expect(notify).toHaveBeenCalledWith(
327
+ expect.stringContaining("已保存为"),
328
+ "info",
329
+ );
297
330
  });
298
331
  });
299
332
 
@@ -312,11 +345,15 @@ describe("createReviewRunner", () => {
312
345
  expect(pi.sendUserMessage).not.toHaveBeenCalled();
313
346
  });
314
347
 
315
- it("switches to the configured model and sends the review prompt", async () => {
348
+ it("shows the picker and runs on the configured model when accepted", async () => {
316
349
  getReviewStore().setModel("anthropic/claude-sonnet-4");
317
350
  const { pi, setModel, sendUserMessage } = fakePi();
318
- const { ctx, notify } = fakeCtx();
351
+ const { ctx, notify, select } = fakeCtx({
352
+ selectResult: "Claude Sonnet 4 (anthropic/claude-sonnet-4)",
353
+ });
319
354
  await createReviewRunner(pi)(ctx);
355
+ // The picker is always shown, even with a configured model.
356
+ expect(select).toHaveBeenCalledTimes(1);
320
357
  expect(setModel).toHaveBeenCalledWith(MODELS[0]);
321
358
  expect(sendUserMessage).toHaveBeenCalledWith(REVIEW_PROMPT);
322
359
  expect(notify).toHaveBeenCalledWith(
@@ -325,6 +362,16 @@ describe("createReviewRunner", () => {
325
362
  );
326
363
  });
327
364
 
365
+ it("runs with the configured model without dialogs when the UI cannot pick", async () => {
366
+ getReviewStore().setModel("anthropic/claude-sonnet-4");
367
+ const { pi, setModel, sendUserMessage } = fakePi();
368
+ const { ctx, select } = fakeCtx({ hasUI: false });
369
+ await createReviewRunner(pi)(ctx);
370
+ expect(select).not.toHaveBeenCalled();
371
+ expect(setModel).toHaveBeenCalledWith(MODELS[0]);
372
+ expect(sendUserMessage).toHaveBeenCalledWith(REVIEW_PROMPT);
373
+ });
374
+
328
375
  it("picks, persists and runs when no model is configured", async () => {
329
376
  const { pi, setModel, sendUserMessage } = fakePi();
330
377
  const { ctx, notify, select } = fakeCtx({
@@ -356,6 +403,12 @@ describe("createReviewRunner", () => {
356
403
  expect(setModel).toHaveBeenCalledWith(MODELS[0]);
357
404
  expect(sendUserMessage).toHaveBeenCalledWith(REVIEW_PROMPT);
358
405
  expect(select).toHaveBeenCalledTimes(1);
406
+ // The title marks the configured model as unavailable (Enter picks the
407
+ // first available row, not the configured one).
408
+ expect(select).toHaveBeenCalledWith(
409
+ "选择评审模型 (当前: anthropic/claude-opus-4-5 不可用)",
410
+ expect.any(Array),
411
+ );
359
412
  });
360
413
 
361
414
  it("aborts when the user cancels the model pick", async () => {
@@ -371,7 +424,9 @@ describe("createReviewRunner", () => {
371
424
  it("reports and aborts when switching to the review model fails", async () => {
372
425
  getReviewStore().setModel("anthropic/claude-sonnet-4");
373
426
  const { pi, setModel, sendUserMessage } = fakePi(false); // setModel → false
374
- const { ctx, notify } = fakeCtx();
427
+ const { ctx, notify } = fakeCtx({
428
+ selectResult: "Claude Sonnet 4 (anthropic/claude-sonnet-4)",
429
+ });
375
430
  await createReviewRunner(pi)(ctx);
376
431
  expect(setModel).toHaveBeenCalledWith(MODELS[0]);
377
432
  expect(sendUserMessage).not.toHaveBeenCalled();
@@ -22,11 +22,6 @@ import { dirname, join } from "node:path";
22
22
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
23
23
  import { REVIEW_FILE } from "./constants.js";
24
24
 
25
- /** Model identity used as the review-model key: "provider/id". */
26
- export function modelKey(model: { provider: string; id: string }): string {
27
- return `${model.provider}/${model.id}`;
28
- }
29
-
30
25
  export class ReviewStore {
31
26
  private model: string | undefined;
32
27
  private readonly file: string;
package/src/index.ts CHANGED
@@ -9,15 +9,18 @@
9
9
  * - favorites — model favorites (ctrl+F/ctrl+J in /model, favorites-only cycling)
10
10
  * - stash — prompt stash (ctrl+alt+y swap / double-press stash-and-clear)
11
11
  * - review — third-party code review with a configured review model (third-review)
12
+ * - compact — dedicated compact model for /compact + auto-compact summaries
12
13
  */
13
14
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
14
15
  import { registerAsk } from "./features/ask/index.js";
16
+ import { registerCompact } from "./features/compact/index.js";
15
17
  import { registerFavorites } from "./features/favorites/index.js";
16
18
  import { registerReview } from "./features/review/index.js";
17
19
  import { registerStash } from "./features/stash/index.js";
18
20
 
19
21
  export default function piToolkits(pi: ExtensionAPI): void {
20
22
  registerAsk(pi);
23
+ registerCompact(pi);
21
24
  registerFavorites(pi);
22
25
  registerReview(pi);
23
26
  registerStash(pi);
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared model-config helpers for features that persist a "provider/id"
3
+ * model key and resolve it against the currently available model list
4
+ * (review, compact-model — favorites keeps its own store-local key logic).
5
+ *
6
+ * Types are derived from pi-coding-agent's own exports (ModelRegistry)
7
+ * rather than imported from @earendil-works/pi-ai: pi-ai 0.87's index.d.ts
8
+ * re-exports `Model`/`Api` via extensioned `./types.ts` specifiers, which
9
+ * tsc 5.9 bundler resolution substitutes to `.d.ts` but some tsserver
10
+ * builds do not. Deriving from ModelRegistry keeps every resolver in
11
+ * agreement with the exact type `ctx.modelRegistry.getAvailable()` returns.
12
+ */
13
+ import type {
14
+ ExtensionContext,
15
+ ModelRegistry,
16
+ } from "@earendil-works/pi-coding-agent";
17
+
18
+ /** The model type pi's ModelRegistry exposes (`Model<Api>`). */
19
+ export type AvailableModel = ReturnType<ModelRegistry["getAvailable"]>[number];
20
+
21
+ /** Model identity used as a persisted config key: "provider/id". */
22
+ export function modelKey(model: { provider: string; id: string }): string {
23
+ return `${model.provider}/${model.id}`;
24
+ }
25
+
26
+ /**
27
+ * Resolve a persisted "provider/id" key against the available model list.
28
+ * Ids may themselves contain slashes (e.g. openrouter/openai/gpt-5), so only
29
+ * the first slash splits provider from id. Returns undefined for missing,
30
+ * malformed, or currently-unavailable keys.
31
+ */
32
+ export function resolveConfiguredModel(
33
+ ctx: ExtensionContext,
34
+ key: string | undefined,
35
+ ): AvailableModel | undefined {
36
+ if (!key) return undefined;
37
+ const slash = key.indexOf("/");
38
+ if (slash <= 0 || slash === key.length - 1) return undefined;
39
+ const provider = key.slice(0, slash);
40
+ const id = key.slice(slash + 1);
41
+ return ctx.modelRegistry
42
+ .getAvailable()
43
+ .find((m) => m.provider === provider && m.id === id);
44
+ }