@andares/pi-toolkits 0.1.0 → 0.3.0

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.
@@ -0,0 +1,232 @@
1
+ /**
2
+ * review feature — third-review command.
3
+ *
4
+ * After a development task finishes, "summon another model" to review the
5
+ * just-completed work: the configured review model takes over the session
6
+ * (full tool access — this is check AND fix, not a read-only audit), runs a
7
+ * review turn, and the session then stays on that model (no auto-restore).
8
+ *
9
+ * Trigger paths (both share createReviewRunner()):
10
+ * - bare message: typing exactly "third-review" / "third review" in the
11
+ * input box (pi.on("input") interception, swallowed with "handled")
12
+ * - slash command: /third-review
13
+ *
14
+ * Review-model config lives in `<agent-dir>/pi-toolkits-review.json`
15
+ * ("provider/id", see store.ts). When no model is configured, or the
16
+ * configured model is gone/unavailable (e.g. deleted from pi's models.json),
17
+ * the user is told the review model doesn't exist and is shown the available
18
+ * model list (ctx.ui.select, same getAvailable() source as /model); the pick
19
+ * is written back to the config and the review runs immediately.
20
+ *
21
+ * Guards: never fires while the agent is streaming (the review needs the
22
+ * just-finished work as context) and never runs two reviews concurrently.
23
+ */
24
+ import type {
25
+ ExtensionAPI,
26
+ ExtensionContext,
27
+ Theme,
28
+ } from "@earendil-works/pi-coding-agent";
29
+ import type { Api, Model } from "@earendil-works/pi-ai";
30
+ import { REVIEW_STATUS_KEY } from "./constants.js";
31
+ import { REVIEW_PROMPT, isReviewTrigger } from "./prompt.js";
32
+ import { getReviewStore, modelKey, type ReviewStore } from "./store.js";
33
+
34
+ /** Resolve the configured review model against the available model list. */
35
+ export function resolveConfiguredModel(
36
+ ctx: ExtensionContext,
37
+ key: string | undefined,
38
+ ): Model<Api> | undefined {
39
+ if (!key) return undefined;
40
+ const slash = key.indexOf("/");
41
+ if (slash <= 0 || slash === key.length - 1) return undefined;
42
+ const provider = key.slice(0, slash);
43
+ const id = key.slice(slash + 1);
44
+ return ctx.modelRegistry
45
+ .getAvailable()
46
+ .find((m) => m.provider === provider && m.id === id);
47
+ }
48
+
49
+ /**
50
+ * Let the user pick a review model from the available list and persist the
51
+ * choice. Returns the picked model, or undefined when cancelled/unavailable.
52
+ *
53
+ * The picker title shows the current config so /review-model doubles as a
54
+ * view-and-change entry point; cancelling leaves the config untouched.
55
+ */
56
+ export async function pickReviewModel(
57
+ ctx: ExtensionContext,
58
+ store: ReviewStore,
59
+ ): Promise<Model<Api> | undefined> {
60
+ // Dialog-capable UI exists in TUI and RPC modes (hasUI); json/print and
61
+ // headless contexts have no dialogs, so fall back to a manual-config hint.
62
+ if (!ctx.hasUI) {
63
+ ctx.ui.notify(
64
+ "third-review: 当前环境不支持模型选择对话框,请编辑 <agent-dir>/pi-toolkits-review.json 手动配置。",
65
+ "error",
66
+ );
67
+ return undefined;
68
+ }
69
+ const available = ctx.modelRegistry.getAvailable();
70
+ if (available.length === 0) {
71
+ ctx.ui.notify(
72
+ "third-review: 当前没有可用模型,请先在 pi 中配置模型。",
73
+ "error",
74
+ );
75
+ return undefined;
76
+ }
77
+ const configured = store.getModel();
78
+ // Same display source as the /model selector: name + [provider]/id.
79
+ const options = available.map((m) => `${m.name} (${m.provider}/${m.id})`);
80
+ const choice = await ctx.ui.select(
81
+ configured
82
+ ? `选择评审模型 (当前: ${configured})`
83
+ : "选择评审模型 (third-review)",
84
+ options,
85
+ );
86
+ if (choice === undefined) {
87
+ ctx.ui.notify("third-review: 已取消,评审模型配置未变更。", "info");
88
+ return undefined;
89
+ }
90
+ const model = available[options.indexOf(choice)];
91
+ if (!model) return undefined;
92
+ store.setModel(modelKey(model));
93
+ return model;
94
+ }
95
+
96
+ /**
97
+ * Create the third-review runner bound to a pi instance.
98
+ *
99
+ * Exported separately from registerReview so tests can drive the flow with
100
+ * fake pi/ctx objects. The returned runner serializes concurrent triggers
101
+ * (one review at a time).
102
+ */
103
+ export function createReviewRunner(
104
+ pi: ExtensionAPI,
105
+ ): (ctx: ExtensionContext) => Promise<void> {
106
+ let running = false;
107
+
108
+ return async (ctx) => {
109
+ if (running) {
110
+ ctx.ui.notify("third-review: 评审已在执行中,请稍候。", "warning");
111
+ return;
112
+ }
113
+ if (!ctx.isIdle()) {
114
+ ctx.ui.notify(
115
+ "third-review: 当前任务仍在进行中,请等待完成后再触发。",
116
+ "warning",
117
+ );
118
+ return;
119
+ }
120
+ running = true;
121
+ try {
122
+ const store = getReviewStore();
123
+ const configured = store.getModel();
124
+ let model = resolveConfiguredModel(ctx, configured);
125
+ if (!model) {
126
+ ctx.ui.notify(
127
+ configured
128
+ ? `third-review: 配置的评审模型 ${configured} 不存在或不可用,请重新选择。`
129
+ : "third-review: 尚未配置评审模型,请选择。",
130
+ "warning",
131
+ );
132
+ model = await pickReviewModel(ctx, store);
133
+ if (!model) return;
134
+ }
135
+ const switched = await pi.setModel(model);
136
+ if (!switched) {
137
+ ctx.ui.notify(
138
+ `third-review: 无法切换到评审模型「${model.name}」(缺少 API key 或鉴权未配置)。`,
139
+ "error",
140
+ );
141
+ return;
142
+ }
143
+ ctx.ui.notify(
144
+ `third-review: 已切换到评审模型「${model.name}」执行评审(查+修);评审完成后会话保持该模型。`,
145
+ "info",
146
+ );
147
+ pi.sendUserMessage(REVIEW_PROMPT);
148
+ } finally {
149
+ running = false;
150
+ }
151
+ };
152
+ }
153
+
154
+ export function registerReview(pi: ExtensionAPI): void {
155
+ const runReview = createReviewRunner(pi);
156
+
157
+ // Bare-message trigger: "third-review" / "third review" in the input box.
158
+ // Extension-injected messages are ignored so the runner's own prompts
159
+ // never re-trigger this path.
160
+ pi.on("input", (event, ctx) => {
161
+ if (event.source === "extension") {
162
+ return { action: "continue" };
163
+ }
164
+ if (isReviewTrigger(event.text)) {
165
+ // Fire-and-forget: the runner uses the live ctx proxy, so it is safe
166
+ // after this handler returns. A session switch mid-dialog can make
167
+ // the context assert-inactive; swallow that instead of leaking an
168
+ // unhandled rejection (a fresh extension instance owns the new
169
+ // session anyway).
170
+ void runReview(ctx).catch((error) => {
171
+ // pi-lens-ignore: no-console-except-error,console-statement, — deliberate degradation log
172
+ console.warn(
173
+ "[pi-toolkits/review] third-review aborted:",
174
+ error instanceof Error ? error.message : String(error),
175
+ );
176
+ });
177
+ return { action: "handled" };
178
+ }
179
+ return { action: "continue" };
180
+ });
181
+
182
+ // Slash-command trigger (pi resolves commands before the input event, so
183
+ // the two paths never double-fire).
184
+ pi.registerCommand("third-review", {
185
+ description:
186
+ "Summon the configured review model to review the just-completed work (check + fix)",
187
+ handler: async (_args, ctx) => {
188
+ await runReview(ctx);
189
+ },
190
+ });
191
+
192
+ // Manual config entry point: view and change the review model. Always
193
+ // opens the picker (title shows the current config); cancelling keeps
194
+ // the existing config, so it is also safe as a view-only command.
195
+ pi.registerCommand("review-model", {
196
+ description:
197
+ "Set or change the review model used by third-review (picker shows the current one)",
198
+ handler: async (_args, ctx) => {
199
+ await pickReviewModel(ctx, getReviewStore());
200
+ },
201
+ });
202
+
203
+ // Footer indicator: muted "review" while a review model is configured.
204
+ let theme: Theme | undefined;
205
+ let latestCtx: ExtensionContext | undefined;
206
+
207
+ const updateFooterStatus = (ctx: ExtensionContext): void => {
208
+ ctx.ui.setStatus(
209
+ REVIEW_STATUS_KEY,
210
+ getReviewStore().getModel() && theme
211
+ ? theme.fg("muted", "review")
212
+ : undefined,
213
+ );
214
+ };
215
+
216
+ pi.on("session_start", (_event, ctx) => {
217
+ latestCtx = ctx;
218
+ try {
219
+ const t = ctx.ui.theme;
220
+ if (t) {
221
+ theme = t;
222
+ }
223
+ } catch {
224
+ // No theme in this mode — footer indicator stays off.
225
+ }
226
+ updateFooterStatus(ctx);
227
+ });
228
+
229
+ getReviewStore().subscribe(() => {
230
+ if (latestCtx) updateFooterStatus(latestCtx);
231
+ });
232
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * review feature — third-party review prompt.
3
+ *
4
+ * The review model keeps FULL tool access: this is "check AND fix", not a
5
+ * read-only audit (confirmed with the user). The prompt is sent as a user
6
+ * message via pi.sendUserMessage(), so the review turn — including any fixes
7
+ * it makes — stays in the current session transcript right next to the work
8
+ * under review.
9
+ *
10
+ * The three focus points the user asked for (本轮需求实现 / 是否有遗漏 /
11
+ * 是否有错误) are spelled out verbatim in the prompt.
12
+ */
13
+
14
+ /** Trigger words for the bare-message form of the third-review command. */
15
+ export const TRIGGER_WORDS: ReadonlySet<string> = new Set([
16
+ "third-review",
17
+ "third review",
18
+ ]);
19
+
20
+ /** Whether the input text is exactly a third-review trigger (case/space-insensitive). */
21
+ export function isReviewTrigger(text: string): boolean {
22
+ return TRIGGER_WORDS.has(text.trim().toLowerCase());
23
+ }
24
+
25
+ export const REVIEW_PROMPT = `请以第三方代码评审的身份,对本次会话中刚完成的开发任务进行 Review。
26
+
27
+ 本轮需求、开发过程和代码改动都在当前会话上下文中,请结合上下文,并用 read/grep 等工具实际检查仓库代码,不要凭空猜测。
28
+
29
+ 请重点检查以下三点(逐项给出结论):
30
+ 1. 本轮需求实现:本轮需求是否已完整、正确地实现?实现与需求描述是否一致?
31
+ 2. 是否有遗漏:需求中是否有遗漏的功能点、边界情况、异常处理、兼容性、测试等?
32
+ 3. 是否有错误:代码中是否存在逻辑错误、潜在 bug、类型/编译错误、安全与性能隐患等?
33
+
34
+ 要求:
35
+ - 对确认的问题直接修复(write/edit),并简要说明改了什么、为什么改;
36
+ - 无法确认或不宜直接改的问题,列出:位置(文件/行)、问题描述、建议方案;
37
+ - 最后输出评审报告:发现问题清单(已修复 / 未修复)、修复说明、总体结论(通过 / 需修改)与剩余风险。`;