@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.
- package/README.md +66 -8
- package/package.json +11 -5
- package/src/features/compact/compact.test.ts +661 -0
- package/src/features/compact/constants.ts +13 -0
- package/src/features/compact/index.ts +371 -0
- package/src/features/compact/prompt.ts +240 -0
- package/src/features/compact/store.ts +137 -0
- package/src/features/favorites/index.ts +5 -3
- package/src/features/favorites/patch-cycle.ts +60 -22
- package/src/features/favorites/patch-selector.ts +19 -20
- package/src/features/review/index.ts +68 -36
- package/src/features/review/review.test.ts +65 -10
- package/src/features/review/store.ts +0 -5
- package/src/index.ts +3 -0
- package/src/lib/model-config.ts +44 -0
|
@@ -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("
|
|
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-
|
|
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-
|
|
293
|
-
|
|
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-
|
|
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("
|
|
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
|
+
}
|