@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.
- package/README.md +203 -24
- package/package.json +7 -3
- package/src/features/favorites/constants.ts +31 -0
- package/src/features/favorites/favorites.test.ts +437 -0
- package/src/features/favorites/index.ts +112 -0
- package/src/features/favorites/patch-cycle.ts +165 -0
- package/src/features/favorites/patch-selector.ts +300 -0
- package/src/features/favorites/store.ts +197 -0
- package/src/features/review/constants.ts +13 -0
- package/src/features/review/index.ts +232 -0
- package/src/features/review/prompt.ts +37 -0
- package/src/features/review/review.test.ts +443 -0
- package/src/features/review/store.ts +137 -0
- package/src/features/stash/index.ts +29 -0
- package/src/features/stash/stash.test.ts +101 -0
- package/src/features/stash/stash.ts +83 -0
- package/src/index.ts +9 -0
|
@@ -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
|
+
- 最后输出评审报告:发现问题清单(已修复 / 未修复)、修复说明、总体结论(通过 / 需修改)与剩余风险。`;
|